Compare commits

...
11 Commits
Author SHA1 Message Date
jbarciabf f65be085cd Merge pull request #138 from BishopFox/azure_improvements
Azure improvements and new modules
2026-11-02 08:22:04 -05:00
Bastien Faure 16320591ea Migrated parts of whoami into tokencache, stuffed existing whoami with entra ID discovery 2026-10-30 21:28:15 -04:00
Bastien Faure 5267f430b0 fix 2026-10-30 20:39:48 -04:00
Bastien Faure 4a63b18fd3 Updated automation in azure, use shared logger 2026-10-30 20:35:07 -04:00
Bastien Faure 0642b82438 Updated functions, use new logger 2026-10-30 20:34:38 -04:00
Bastien Faure e688d79ed2 Migrated azure commands to shared logger 2026-10-30 17:37:16 -04:00
Bastien Faure 2f0cfd7b1f Added Azure automation command 2026-10-29 23:55:08 -04:00
Bastien Faure e3a1803cce Added Azure Func module 2026-10-29 23:54:16 -04:00
Bastien Faure 6257da9391 Added ACR module for Azure 2026-10-29 23:51:47 -04:00
Bastien Faure d44899f213 Merge pull request #134 from sethsec/fix-region-slowness
Fix region slowness and hangs with opt-in/unreachable regions
2026-08-20 14:39:52 -04:00
Seth Art 29ab782b31 Fix region slowness and hangs with opt-in/unreachable regions
Commands were hanging for several minutes when run with low-privilege
credentials against accounts where opt-in regions (e.g. me-south-1)
are not enabled. Each unreachable region exhausted the full AWS SDK
retry budget (3 attempts × ~30s timeout) per service call, compounding
across every check in all-checks mode.

Three-part fix:

1. TCP reachability probe: Before making any API calls, probe
   sts.REGION.amazonaws.com:443 with a 3-second timeout. Unreachable
   regions are filtered out concurrently and cached for the run so the
   probe only happens once per region. This eliminates multi-minute
   stalls caused by regions that are not enabled for the account.

2. --max-retries flag: New persistent flag on the aws subcommand
   (default 3, matching prior behaviour). Setting --max-retries 0
   skips all retries, useful when running black-box against an account
   where most API calls will return 403/401 anyway.

3. Remove me-south-1 from the static fallback region list: This
   Bahrain opt-in region was the primary offender reported in issue
   #132. It is excluded from the hardcoded list so accounts that never
   opted in skip it entirely even before the TCP probe runs.

Also fixes a pre-existing bug in GetIamSimResult where a redundant
second isRoleAdminBool check after the cache-or-compute block would
overwrite a correct "YES" result from the cache with "No" (because
isRoleAdminBool stays false on a cache hit).

Fixes #132
2026-06-17 14:29:10 -04:00
25 changed files with 1928 additions and 167 deletions

No files matched your search

+1 -7
View File
@@ -19,7 +19,7 @@ var yellow = color.New(color.FgRed).SprintFunc()
var blue = color.New(color.FgBlue).SprintFunc()
var magenta = color.New(color.FgMagenta).SprintFunc()
var green = color.New(color.FgGreen).SprintFunc()
var AWSRegions = []string{"us-east-1", "us-east-2", "us-west-1", "us-west-2", "af-south-1", "ap-east-1", "ap-south-1", "ap-northeast-3", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-northeast-1", "ca-central-1", "eu-central-1", "eu-west-1", "eu-west-2", "eu-south-1", "eu-west-3", "eu-north-1", "me-south-1", "sa-east-1"}
var AWSRegions = []string{"us-east-1", "us-east-2", "us-west-1", "us-west-2", "af-south-1", "ap-east-1", "ap-south-1", "ap-northeast-3", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-northeast-1", "ca-central-1", "eu-central-1", "eu-west-1", "eu-west-2", "eu-south-1", "eu-west-3", "eu-north-1", "sa-east-1"}
var sharedLogger = internal.TxtLogger()
func GetIamSimResult(SkipAdminCheck bool, roleArnPtr *string, iamSimulatorMod IamSimulatorModule, localAdminMap map[string]bool) (string, string) {
@@ -54,12 +54,6 @@ func GetIamSimResult(SkipAdminCheck bool, roleArnPtr *string, iamSimulatorMod Ia
}
}
if isRoleAdminBool {
adminRole = "YES"
} else {
adminRole = "No"
}
} else {
adminRole = "Skipped"
canRolePrivEsc = "Skipped"
+167
View File
@@ -0,0 +1,167 @@
package azure
import (
"context"
"fmt"
"path/filepath"
"sort"
"strings"
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/subscriptions"
containerregistry "github.com/Azure/azure-sdk-for-go/services/preview/containerregistry/mgmt/2022-02-01-preview/containerregistry"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/smithy-go/ptr"
)
const acrDockerTokenUsername = "00000000-0000-0000-0000-000000000000"
var getContainerRegistries = getContainerRegistriesOriginal
func AzACRCommand(AzTenantID, AzSubscription, AzOutputDirectory, Version string, AzVerbosity int, AzWrapTable bool, AzMergedTable bool) error {
if AzTenantID != "" && AzSubscription == "" {
tenantInfo := populateTenant(AzTenantID)
if ptr.ToString(tenantInfo.ID) == "" {
return fmt.Errorf("could not find Azure tenant %q", AzTenantID)
}
if !AzMergedTable {
for _, subscription := range GetSubscriptionsPerTenantID(ptr.ToString(tenantInfo.ID)) {
if err := runACRCommandForSingleSubscription(ptr.ToString(subscription.SubscriptionID), AzOutputDirectory, AzVerbosity, AzWrapTable, Version); err != nil {
return err
}
}
return nil
}
azureLogger.InfoM(fmt.Sprintf("Enumerating container registries for tenant %s (%s)", ptr.ToString(tenantInfo.DefaultDomain), ptr.ToString(tenantInfo.ID)), globals.AZ_ACR_MODULE_NAME)
var body [][]string
var loot strings.Builder
for _, subscription := range GetSubscriptionsPerTenantID(ptr.ToString(tenantInfo.ID)) {
rows, commands, err := getACRData(subscription)
if err != nil {
return err
}
body = append(body, rows...)
loot.WriteString(commands)
}
writeACROutput(AzOutputDirectory, ptr.ToString(tenantInfo.DefaultDomain), "1-tenant-level", ptr.ToString(tenantInfo.DefaultDomain), body, loot.String(), AzVerbosity, AzWrapTable)
return nil
}
if AzTenantID == "" && AzSubscription != "" {
return runACRCommandForSingleSubscription(AzSubscription, AzOutputDirectory, AzVerbosity, AzWrapTable, Version)
}
return fmt.Errorf("please specify either --tenant or --subscription")
}
func runACRCommandForSingleSubscription(AzSubscription, AzOutputDirectory string, AzVerbosity int, AzWrapTable bool, Version string) error {
subscriptionInfo := PopulateSubsriptionType(AzSubscription)
if subscriptionInfo.ID == "" {
return fmt.Errorf("could not find Azure subscription %q", AzSubscription)
}
tenantID := ptr.ToString(GetTenantIDPerSubscription(subscriptionInfo.ID))
tenantInfo := populateTenant(tenantID)
azureLogger.InfoM(fmt.Sprintf("Enumerating container registries for subscription %s (%s)", subscriptionInfo.Name, subscriptionInfo.ID), globals.AZ_ACR_MODULE_NAME)
var selectedSubscription subscriptions.Subscription
for _, subscription := range GetSubscriptions() {
if ptr.ToString(subscription.SubscriptionID) == subscriptionInfo.ID {
selectedSubscription = subscription
break
}
}
rows, loot, err := getACRData(selectedSubscription)
if err != nil {
return err
}
writeACROutput(AzOutputDirectory, ptr.ToString(tenantInfo.DefaultDomain), subscriptionInfo.Name, subscriptionInfo.Name, rows, loot, AzVerbosity, AzWrapTable)
return nil
}
func getACRData(subscription subscriptions.Subscription) ([][]string, string, error) {
subscriptionID := ptr.ToString(subscription.SubscriptionID)
registries, err := getContainerRegistries(subscriptionID)
if err != nil {
return nil, "", fmt.Errorf("could not enumerate container registries for subscription %s: %w", subscriptionID, err)
}
return getACRRelevantData(subscription, registries)
}
func getACRRelevantData(subscription subscriptions.Subscription, registries []containerregistry.Registry) ([][]string, string, error) {
var body [][]string
var loot strings.Builder
subscriptionID := ptr.ToString(subscription.SubscriptionID)
subscriptionName := ptr.ToString(subscription.DisplayName)
for _, registry := range registries {
name := ptr.ToString(registry.Name)
loginServer := ""
anonymousPulls := false
if registry.RegistryProperties != nil {
loginServer = ptr.ToString(registry.LoginServer)
anonymousPulls = ptr.ToBool(registry.AnonymousPullEnabled)
}
body = append(body, []string{name, subscriptionName, subscriptionID, loginServer, fmt.Sprintf("%t", anonymousPulls)})
loot.WriteString(acrLootCommands(name, subscriptionID, loginServer))
}
sort.Slice(body, func(i, j int) bool {
if body[i][1] == body[j][1] {
return body[i][0] < body[j][0]
}
return body[i][1] < body[j][1]
})
return body, loot.String(), nil
}
func acrLootCommands(registryName, subscriptionID, loginServer string) string {
if registryName == "" || subscriptionID == "" || loginServer == "" {
return ""
}
return fmt.Sprintf("# %s (%s)\nACR_TOKEN=$(az acr login -n %s --subscription %s --expose-token --output tsv --query accessToken)\nprintf '%%s' \"$ACR_TOKEN\" | docker login %s --username %s --password-stdin\n\n",
registryName, loginServer, registryName, subscriptionID, loginServer, acrDockerTokenUsername)
}
func writeACROutput(outputDirectory, tenantDomain, directorySuffix, prefix string, body [][]string, loot string, verbosity int, wrap bool) {
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: globals.AZ_ACR_MODULE_NAME,
PrefixIdentifier: prefix,
Table: internal.TableClient{
Wrap: wrap,
DirectoryName: filepath.Join(outputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, tenantDomain, directorySuffix),
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: []string{"Registry Name", "Subscription Name", "Subscription ID", "Login Server", "Anonymous Pulls"},
Body: body,
Name: globals.AZ_ACR_MODULE_NAME,
})
if loot != "" {
o.Loot.DirectoryName = filepath.Join(outputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, tenantDomain, directorySuffix, "loot")
o.Loot.LootFiles = append(o.Loot.LootFiles, internal.LootFile{Contents: loot, Name: "acr-login-commands"})
o.WriteFullOutput(o.Table.TableFiles, o.Loot.LootFiles)
} else {
o.WriteFullOutput(o.Table.TableFiles, nil)
}
fmt.Println()
}
func getContainerRegistriesOriginal(subscriptionID string) ([]containerregistry.Registry, error) {
client := internal.GetContainerRegistriesClient(subscriptionID)
page, err := client.List(context.TODO())
if err != nil {
return nil, err
}
var registries []containerregistry.Registry
for page.NotDone() {
registries = append(registries, page.Values()...)
if err := page.NextWithContext(context.TODO()); err != nil {
return nil, err
}
}
return registries, nil
}
+42
View File
@@ -0,0 +1,42 @@
package azure
import (
"strings"
"testing"
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/subscriptions"
containerregistry "github.com/Azure/azure-sdk-for-go/services/preview/containerregistry/mgmt/2022-02-01-preview/containerregistry"
"github.com/aws/smithy-go/ptr"
)
func TestGetACRRelevantData(t *testing.T) {
registries := []containerregistry.Registry{
{
Name: ptr.String("packnet"),
RegistryProperties: &containerregistry.RegistryProperties{
LoginServer: ptr.String("packnet.azurecr.io"),
AnonymousPullEnabled: ptr.Bool(true),
},
},
}
subscription := subscriptions.Subscription{
SubscriptionID: ptr.String("8bb4e6f5-be51-4eab-bd68-ecf6fd08338f"),
DisplayName: ptr.String("Production"),
}
body, loot, err := getACRRelevantData(subscription, registries)
if err != nil {
t.Fatalf("getACRRelevantData returned an error: %v", err)
}
if len(body) != 1 || body[0][0] != "packnet" || body[0][3] != "packnet.azurecr.io" || body[0][4] != "true" {
t.Fatalf("unexpected ACR table body: %+v", body)
}
for _, expected := range []string{
"az acr login -n packnet --subscription 8bb4e6f5-be51-4eab-bd68-ecf6fd08338f --expose-token",
"docker login packnet.azurecr.io --username " + acrDockerTokenUsername + " --password-stdin",
} {
if !strings.Contains(loot, expected) {
t.Errorf("loot does not contain %q:\n%s", expected, loot)
}
}
}
+243
View File
@@ -0,0 +1,243 @@
package azure
import (
"context"
"encoding/json"
"fmt"
"path/filepath"
"sort"
"strings"
"github.com/Azure/azure-sdk-for-go/profiles/latest/automation/mgmt/automation"
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/subscriptions"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/smithy-go/ptr"
)
var (
getAutomationAccounts = getAutomationAccountsOriginal
getAutomationAssets = getAutomationAssetsOriginal
)
type automationAssets struct {
Runbooks []automation.Runbook `json:"runbooks"`
RunbookSources map[string]string `json:"-"`
Credentials []automation.Credential `json:"credentials"`
Webhooks []automation.Webhook `json:"webhooks"`
}
type automationLoot struct {
Runbooks string
Credentials string
Webhooks string
}
func AzAutomationCommand(tenantID, subscription, outputDirectory, version string, verbosity int, wrap, merged bool) error {
if tenantID != "" && subscription == "" {
tenant := populateTenant(tenantID)
if ptr.ToString(tenant.ID) == "" {
return fmt.Errorf("could not find Azure tenant %q", tenantID)
}
azureLogger.InfoM(fmt.Sprintf("Enumerating Automation Accounts for tenant %s (%s)", ptr.ToString(tenant.DefaultDomain), ptr.ToString(tenant.ID)), globals.AZ_AUTOMATION_MODULE_NAME)
subs := GetSubscriptionsPerTenantID(ptr.ToString(tenant.ID))
if !merged {
for _, sub := range subs {
if err := runAutomationForSubscription(sub, outputDirectory, verbosity, wrap); err != nil {
return err
}
}
return nil
}
var rows [][]string
var loot automationLoot
for _, sub := range subs {
subRows, subLoot, warnings, err := getAutomationData(sub)
if err != nil {
return err
}
rows = append(rows, subRows...)
loot.Runbooks += subLoot.Runbooks
loot.Credentials += subLoot.Credentials
loot.Webhooks += subLoot.Webhooks
for _, warning := range warnings {
azureLogger.WarnM(warning.Error(), globals.AZ_AUTOMATION_MODULE_NAME)
}
}
if len(subs) > 0 {
writeAutomationOutput(outputDirectory, ptr.ToString(tenant.DefaultDomain), "1-tenant-level", ptr.ToString(tenant.DefaultDomain), rows, loot, verbosity, wrap)
}
return nil
}
if tenantID == "" && subscription != "" {
id := ptr.ToString(GetSubscriptionID(subscription))
for _, sub := range GetSubscriptions() {
if ptr.ToString(sub.SubscriptionID) == id {
return runAutomationForSubscription(sub, outputDirectory, verbosity, wrap)
}
}
return fmt.Errorf("could not find Azure subscription %q", subscription)
}
return fmt.Errorf("please specify either --tenant or --subscription")
}
func runAutomationForSubscription(sub subscriptions.Subscription, outputDirectory string, verbosity int, wrap bool) error {
azureLogger.InfoM(fmt.Sprintf("Enumerating Automation Accounts for subscription %s (%s)", ptr.ToString(sub.DisplayName), ptr.ToString(sub.SubscriptionID)), globals.AZ_AUTOMATION_MODULE_NAME)
rows, loot, warnings, err := getAutomationData(sub)
if err != nil {
return err
}
for _, warning := range warnings {
azureLogger.WarnM(warning.Error(), globals.AZ_AUTOMATION_MODULE_NAME)
}
tenant := populateTenant(ptr.ToString(sub.TenantID))
writeAutomationOutput(outputDirectory, ptr.ToString(tenant.DefaultDomain), ptr.ToString(sub.DisplayName), ptr.ToString(sub.DisplayName), rows, loot, verbosity, wrap)
return nil
}
func getAutomationData(subscription subscriptions.Subscription) ([][]string, automationLoot, []error, error) {
subscriptionID := ptr.ToString(subscription.SubscriptionID)
accounts, err := getAutomationAccounts(subscriptionID)
if err != nil {
return nil, automationLoot{}, nil, fmt.Errorf("could not enumerate Automation Accounts for subscription %s: %w", subscriptionID, err)
}
var rows [][]string
var loot automationLoot
var warnings []error
for _, account := range accounts {
name := ptr.ToString(account.Name)
rg := resourceGroupFromID(ptr.ToString(account.ID))
assets, err := getAutomationAssets(subscriptionID, rg, name)
if err != nil {
warnings = append(warnings, fmt.Errorf("could not enumerate assets for Automation Account %s: %w", name, err))
continue
}
rows = append(rows, []string{name, ptr.ToString(subscription.DisplayName), subscriptionID, rg, fmt.Sprintf("%d", len(assets.Runbooks)), fmt.Sprintf("%d", len(assets.Credentials)), fmt.Sprintf("%d", len(assets.Webhooks))})
if len(assets.Runbooks)+len(assets.Credentials)+len(assets.Webhooks) > 0 {
accountLoot := formatAutomationLoot(subscriptionID, name, rg, assets)
loot.Runbooks += accountLoot.Runbooks
loot.Credentials += accountLoot.Credentials
loot.Webhooks += accountLoot.Webhooks
}
}
sort.Slice(rows, func(i, j int) bool { return rows[i][0] < rows[j][0] })
return rows, loot, warnings, nil
}
func formatAutomationLoot(subscriptionID, accountName, resourceGroup string, assets automationAssets) automationLoot {
var loot automationLoot
var runbooks, credentials, webhooks strings.Builder
for _, runbook := range assets.Runbooks {
name := ptr.ToString(runbook.Name)
fmt.Fprintf(&runbooks, "===============================================================\nAutomation Account: %s\nResource Group: %s\nRunbook: %s\n\n%s\n\n", accountName, resourceGroup, name, assets.RunbookSources[name])
}
for _, credential := range assets.Credentials {
name := ptr.ToString(credential.Name)
value, _ := json.MarshalIndent(map[string]any{"name": name, "properties": credential.CredentialProperties}, "", " ")
fmt.Fprintf(&credentials, "az automation credential show --automation-account-name %q --resource-group %q --name %q --subscription %q\n%s\n\n", accountName, resourceGroup, name, subscriptionID, value)
}
for _, webhook := range assets.Webhooks {
name := ptr.ToString(webhook.Name)
value, _ := json.MarshalIndent(map[string]any{"name": name, "properties": webhook.WebhookProperties}, "", " ")
fmt.Fprintf(&webhooks, "az automation webhook show --automation-account-name %q --resource-group %q --name %q --subscription %q\n%s\n\n", accountName, resourceGroup, name, subscriptionID, value)
}
loot.Runbooks = runbooks.String()
loot.Credentials = credentials.String()
loot.Webhooks = webhooks.String()
return loot
}
func writeAutomationOutput(outputDirectory, tenantDomain, directorySuffix, prefix string, rows [][]string, loot automationLoot, verbosity int, wrap bool) {
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: globals.AZ_AUTOMATION_MODULE_NAME,
PrefixIdentifier: prefix,
Table: internal.TableClient{
Wrap: wrap,
DirectoryName: filepath.Join(outputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, tenantDomain, directorySuffix),
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: []string{"Automation Account", "Subscription Name", "Subscription ID", "Resource Group", "Runbooks", "Credentials", "Webhooks"},
Body: rows,
Name: globals.AZ_AUTOMATION_MODULE_NAME,
})
o.Loot.DirectoryName = filepath.Join(o.Table.DirectoryName, "loot")
if loot.Runbooks != "" {
o.Loot.LootFiles = append(o.Loot.LootFiles, internal.LootFile{Name: "automation-runbooks", Contents: loot.Runbooks})
}
if loot.Credentials != "" {
o.Loot.LootFiles = append(o.Loot.LootFiles, internal.LootFile{Name: "automation-credentials", Contents: loot.Credentials})
}
if loot.Webhooks != "" {
o.Loot.LootFiles = append(o.Loot.LootFiles, internal.LootFile{Name: "automation-webhooks", Contents: loot.Webhooks})
}
if len(o.Loot.LootFiles) > 0 {
o.WriteFullOutput(o.Table.TableFiles, o.Loot.LootFiles)
} else {
o.WriteFullOutput(o.Table.TableFiles, nil)
}
fmt.Println()
}
func getAutomationAccountsOriginal(subscriptionID string) ([]automation.Account, error) {
accountsClient, _, _, _ := internal.GetAutomationClients(subscriptionID)
page, err := accountsClient.List(context.TODO())
if err != nil {
return nil, err
}
var accounts []automation.Account
for page.NotDone() {
accounts = append(accounts, page.Values()...)
if err := page.NextWithContext(context.TODO()); err != nil {
return nil, err
}
}
return accounts, nil
}
func getAutomationAssetsOriginal(subscriptionID, resourceGroup, accountName string) (automationAssets, error) {
_, runbooksClient, credentialsClient, webhooksClient := internal.GetAutomationClients(subscriptionID)
var assets automationAssets
runbooks, err := runbooksClient.ListByAutomationAccount(context.TODO(), resourceGroup, accountName)
if err != nil {
return assets, err
}
for runbooks.NotDone() {
assets.Runbooks = append(assets.Runbooks, runbooks.Values()...)
if err := runbooks.NextWithContext(context.TODO()); err != nil {
return assets, err
}
}
assets.RunbookSources = make(map[string]string, len(assets.Runbooks))
for _, runbook := range assets.Runbooks {
name := ptr.ToString(runbook.Name)
content, contentErr := runbooksClient.GetContent(context.TODO(), resourceGroup, accountName, name)
if contentErr != nil {
assets.RunbookSources[name] = fmt.Sprintf("[source unavailable: %s]", contentErr)
continue
}
assets.RunbookSources[name] = ptr.ToString(content.Value)
}
credentials, err := credentialsClient.ListByAutomationAccount(context.TODO(), resourceGroup, accountName)
if err != nil {
return assets, err
}
for credentials.NotDone() {
assets.Credentials = append(assets.Credentials, credentials.Values()...)
if err := credentials.NextWithContext(context.TODO()); err != nil {
return assets, err
}
}
webhooks, err := webhooksClient.ListByAutomationAccount(context.TODO(), resourceGroup, accountName, "")
if err != nil {
return assets, err
}
for webhooks.NotDone() {
assets.Webhooks = append(assets.Webhooks, webhooks.Values()...)
if err := webhooks.NextWithContext(context.TODO()); err != nil {
return assets, err
}
}
return assets, nil
}
+45
View File
@@ -0,0 +1,45 @@
package azure
import (
"strings"
"testing"
"github.com/Azure/azure-sdk-for-go/profiles/latest/automation/mgmt/automation"
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/subscriptions"
"github.com/aws/smithy-go/ptr"
)
func TestGetAutomationDataCountsAndLoot(t *testing.T) {
originalAccounts, originalAssets := getAutomationAccounts, getAutomationAssets
t.Cleanup(func() { getAutomationAccounts, getAutomationAssets = originalAccounts, originalAssets })
getAutomationAccounts = func(string) ([]automation.Account, error) {
return []automation.Account{{Name: ptr.String("operations"), ID: ptr.String("/subscriptions/sub/resourceGroups/ops/providers/Microsoft.Automation/automationAccounts/operations")}}, nil
}
getAutomationAssets = func(_, resourceGroup, account string) (automationAssets, error) {
if resourceGroup != "ops" || account != "operations" {
t.Fatalf("unexpected automation target %s/%s", resourceGroup, account)
}
return automationAssets{Runbooks: []automation.Runbook{{Name: ptr.String("daily")}, {Name: ptr.String("weekly")}}, RunbookSources: map[string]string{"daily": "Write-Output 'daily'", "weekly": "print('weekly')"}, Credentials: []automation.Credential{{Name: ptr.String("service-account")}}, Webhooks: []automation.Webhook{{Name: ptr.String("deploy")}}}, nil
}
sub := subscriptions.Subscription{SubscriptionID: ptr.String("sub"), DisplayName: ptr.String("Production")}
rows, loot, warnings, err := getAutomationData(sub)
if err != nil || len(warnings) != 0 {
t.Fatalf("unexpected errors: err=%v warnings=%v", err, warnings)
}
if len(rows) != 1 || rows[0][4] != "2" || rows[0][5] != "1" || rows[0][6] != "1" {
t.Fatalf("unexpected automation rows: %+v", rows)
}
if !strings.Contains(loot.Runbooks, "Runbook: daily") || !strings.Contains(loot.Runbooks, "Write-Output 'daily'") {
t.Errorf("runbook loot is incomplete: %s", loot.Runbooks)
}
for _, value := range []string{"az automation credential show", "service-account", `"name": "service-account"`} {
if !strings.Contains(loot.Credentials, value) {
t.Errorf("credential loot missing %q: %s", value, loot.Credentials)
}
}
for _, value := range []string{"az automation webhook show", "deploy", `"name": "deploy"`} {
if !strings.Contains(loot.Webhooks, value) {
t.Errorf("webhook loot missing %q: %s", value, loot.Webhooks)
}
}
}
+526
View File
@@ -0,0 +1,526 @@
package azure
import (
"context"
"encoding/json"
"fmt"
"path/filepath"
"sort"
"strings"
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/subscriptions"
"github.com/Azure/azure-sdk-for-go/profiles/latest/web/mgmt/web"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/smithy-go/ptr"
)
var (
getFunctionApps = getFunctionAppsOriginal
getFunctionConfiguration = getFunctionConfigurationOriginal
getFunctions = getFunctionsOriginal
)
type functionConfiguration struct {
AppSettings map[string]*string
ConnectionStrings map[string]*web.ConnStringValueTypePair
AuthSettings web.SiteAuthSettingsV2
}
type functionLoot struct {
Configuration string
TestData string
}
func AzFuncCommand(tenantID, subscription, outputDirectory, version string, verbosity int, wrap, merged bool) error {
if tenantID != "" && subscription == "" {
tenant := populateTenant(tenantID)
if ptr.ToString(tenant.ID) == "" {
return fmt.Errorf("could not find Azure tenant %q", tenantID)
}
azureLogger.InfoM(fmt.Sprintf("Enumerating Function Apps for tenant %s (%s)", ptr.ToString(tenant.DefaultDomain), ptr.ToString(tenant.ID)), globals.AZ_FUNC_MODULE_NAME)
subs := GetSubscriptionsPerTenantID(ptr.ToString(tenant.ID))
if !merged {
for _, sub := range subs {
if err := runFuncForSubscription(sub, outputDirectory, verbosity, wrap); err != nil {
return err
}
}
return nil
}
var rows [][]string
var loot functionLoot
for _, sub := range subs {
subRows, subLoot, warnings, err := getFunctionData(sub)
if err != nil {
return err
}
rows = append(rows, subRows...)
loot.Configuration += subLoot.Configuration
loot.TestData += subLoot.TestData
for _, warning := range warnings {
azureLogger.WarnM(warning.Error(), globals.AZ_FUNC_MODULE_NAME)
}
}
if len(subs) > 0 {
writeFunctionOutput(outputDirectory, ptr.ToString(tenant.DefaultDomain), "1-tenant-level", ptr.ToString(tenant.DefaultDomain), rows, loot, verbosity, wrap)
}
return nil
}
if tenantID == "" && subscription != "" {
id := ptr.ToString(GetSubscriptionID(subscription))
for _, sub := range GetSubscriptions() {
if ptr.ToString(sub.SubscriptionID) == id {
return runFuncForSubscription(sub, outputDirectory, verbosity, wrap)
}
}
return fmt.Errorf("could not find Azure subscription %q", subscription)
}
return fmt.Errorf("please specify either --tenant or --subscription")
}
func runFuncForSubscription(sub subscriptions.Subscription, outputDirectory string, verbosity int, wrap bool) error {
azureLogger.InfoM(fmt.Sprintf("Enumerating Function Apps for subscription %s (%s)", ptr.ToString(sub.DisplayName), ptr.ToString(sub.SubscriptionID)), globals.AZ_FUNC_MODULE_NAME)
rows, loot, warnings, err := getFunctionData(sub)
if err != nil {
return err
}
for _, warning := range warnings {
azureLogger.WarnM(warning.Error(), globals.AZ_FUNC_MODULE_NAME)
}
tenant := populateTenant(ptr.ToString(sub.TenantID))
writeFunctionOutput(outputDirectory, ptr.ToString(tenant.DefaultDomain), ptr.ToString(sub.DisplayName), ptr.ToString(sub.DisplayName), rows, loot, verbosity, wrap)
return nil
}
func getFunctionData(subscription subscriptions.Subscription) ([][]string, functionLoot, []error, error) {
subscriptionID := ptr.ToString(subscription.SubscriptionID)
apps, err := getFunctionApps(subscriptionID)
if err != nil {
return nil, functionLoot{}, nil, fmt.Errorf("could not enumerate Function Apps for subscription %s: %w", subscriptionID, err)
}
var rows [][]string
var loot functionLoot
var warnings []error
for _, app := range apps {
if !strings.Contains(strings.ToLower(ptr.ToString(app.Kind)), "functionapp") {
continue
}
name := ptr.ToString(app.Name)
host := ""
if app.SiteProperties != nil {
host = ptr.ToString(app.DefaultHostName)
}
appURL := host
if host != "" {
appURL = "https://" + host
}
rg := resourceGroupFromID(ptr.ToString(app.ID))
config, err := getFunctionConfiguration(subscriptionID, rg, name)
if err != nil {
warnings = append(warnings, fmt.Errorf("could not read configuration for Function App %s: %w", name, err))
}
providers := functionAuthProviders(config.AuthSettings)
functions, err := getFunctions(subscriptionID, rg, name)
if err != nil {
warnings = append(warnings, fmt.Errorf("could not enumerate functions for Function App %s: %w", name, err))
rows = append(rows, functionAppFallbackRow(name, subscription, appURL, providers))
} else if len(functions) == 0 {
rows = append(rows, functionAppFallbackRow(name, subscription, appURL, providers))
} else {
for _, function := range functions {
functionName := lastResourceIDSegment(ptr.ToString(function.Name))
trigger := "Unknown"
state := "Unknown"
if function.FunctionEnvelopeProperties != nil {
trigger = functionTrigger(function.Config, ptr.ToString(function.InvokeURLTemplate))
state = "Enabled"
if ptr.ToBool(function.IsDisabled) {
state = "Disabled"
}
loot.TestData += formatFunctionTestDataLoot(subscriptionID, rg, name, functionName, ptr.ToString(function.TestData))
}
authMethods := append(functionAuthLevels(function.Config), providers...)
if len(authMethods) == 0 {
authMethods = []string{"Unknown"}
}
rows = append(rows, []string{name, functionName, trigger, strings.Join(uniqueStrings(authMethods), ", "), state, ptr.ToString(subscription.DisplayName), subscriptionID})
}
}
loot.Configuration += formatFunctionLoot(name, appURL, config)
}
sort.Slice(rows, func(i, j int) bool {
if rows[i][0] == rows[j][0] {
return rows[i][1] < rows[j][1]
}
return rows[i][0] < rows[j][0]
})
return rows, loot, warnings, nil
}
func functionAppFallbackRow(name string, subscription subscriptions.Subscription, appURL string, providers []string) []string {
authentication := strings.Join(providers, ", ")
if authentication == "" {
authentication = "Unknown"
}
trigger := "Unknown"
if appURL != "" {
trigger = "App URL: " + appURL
}
return []string{name, "", trigger, authentication, "Unknown", ptr.ToString(subscription.DisplayName), ptr.ToString(subscription.SubscriptionID)}
}
func functionTableHeader() []string {
return []string{"Function App", "Function Name", "Trigger", "Authentication", "State", "Subscription Name", "Subscription ID"}
}
func functionVisibleColumns() []string {
return []string{"Function App", "Function Name", "Trigger", "Authentication", "State", "Subscription Name"}
}
func formatFunctionLoot(name, appURL string, config functionConfiguration) string {
if len(config.AppSettings) == 0 && len(config.ConnectionStrings) == 0 {
return ""
}
var loot strings.Builder
fmt.Fprintf(&loot, "===============================================================\nFunction App: %s\nURL: %s\n\n", name, appURL)
fmt.Fprintf(&loot, "App Service Authentication: %s\n\n", strings.Join(functionAuthProviders(config.AuthSettings), ", "))
loot.WriteString("App Settings:\n")
settings := sortedKeys(config.AppSettings)
for _, key := range settings {
fmt.Fprintf(&loot, "%s=%s\n", key, ptr.ToString(config.AppSettings[key]))
}
loot.WriteString("\nConnection Strings:\n")
connections := make([]string, 0, len(config.ConnectionStrings))
for key := range config.ConnectionStrings {
connections = append(connections, key)
}
sort.Strings(connections)
for _, key := range connections {
value := config.ConnectionStrings[key]
if value != nil {
fmt.Fprintf(&loot, "%s=%s (type=%s)\n", key, ptr.ToString(value.Value), value.Type)
}
}
loot.WriteString("\n")
return loot.String()
}
func functionAuthProviders(settings web.SiteAuthSettingsV2) []string {
if settings.SiteAuthSettingsV2Properties == nil || settings.Platform == nil || !ptr.ToBool(settings.Platform.Enabled) {
return nil
}
providers := settings.IdentityProviders
if providers == nil {
return []string{"App Service Authentication enabled"}
}
var methods []string
if providers.AzureActiveDirectory != nil && ptr.ToBool(providers.AzureActiveDirectory.Enabled) {
methods = append(methods, "Entra ID")
}
if providers.Facebook != nil && ptr.ToBool(providers.Facebook.Enabled) {
methods = append(methods, "Facebook")
}
if providers.GitHub != nil && ptr.ToBool(providers.GitHub.Enabled) {
methods = append(methods, "GitHub")
}
if providers.Google != nil && ptr.ToBool(providers.Google.Enabled) {
methods = append(methods, "Google")
}
if providers.LegacyMicrosoftAccount != nil && ptr.ToBool(providers.LegacyMicrosoftAccount.Enabled) {
methods = append(methods, "Microsoft Account")
}
if providers.Twitter != nil && ptr.ToBool(providers.Twitter.Enabled) {
methods = append(methods, "Twitter")
}
if providers.Apple != nil && ptr.ToBool(providers.Apple.Enabled) {
methods = append(methods, "Apple")
}
if providers.AzureStaticWebApps != nil && ptr.ToBool(providers.AzureStaticWebApps.Enabled) {
methods = append(methods, "Azure Static Web Apps")
}
for name, provider := range providers.CustomOpenIDConnectProviders {
if provider != nil && ptr.ToBool(provider.Enabled) {
methods = append(methods, "OpenID Connect: "+name)
}
}
sort.Strings(methods)
return methods
}
func functionAuthLevels(config any) []string {
encoded, err := json.Marshal(config)
if err != nil {
return nil
}
var value any
if json.Unmarshal(encoded, &value) != nil {
return nil
}
var levels []string
var visit func(any)
visit = func(current any) {
switch typed := current.(type) {
case map[string]any:
for key, child := range typed {
if strings.EqualFold(key, "authLevel") {
if level, ok := child.(string); ok {
levels = append(levels, formatFunctionAuthLevel(level))
}
} else {
visit(child)
}
}
case []any:
for _, child := range typed {
visit(child)
}
}
}
visit(value)
return uniqueStrings(levels)
}
func functionTrigger(config any, invokeURL string) string {
encoded, err := json.Marshal(config)
if err != nil {
return "Unknown"
}
var value any
if json.Unmarshal(encoded, &value) != nil {
return "Unknown"
}
result := ""
var visit func(any)
visit = func(current any) {
if result != "" {
return
}
switch typed := current.(type) {
case map[string]any:
bindingType := mapStringValue(typed, "type")
if strings.HasSuffix(strings.ToLower(bindingType), "trigger") {
label := functionTriggerLabel(bindingType)
detail := functionTriggerDetail(bindingType, typed, invokeURL)
result = label
if detail != "" {
result += ": " + detail
}
return
}
for _, child := range typed {
visit(child)
}
case []any:
for _, child := range typed {
visit(child)
}
}
}
visit(value)
if result == "" {
return "Unknown"
}
return result
}
func functionTriggerLabel(bindingType string) string {
switch strings.ToLower(bindingType) {
case "httptrigger":
return "HTTP"
case "timertrigger":
return "Time Trigger"
case "blobtrigger":
return "Blob Trigger"
case "queuetrigger":
return "Queue Trigger"
case "servicebustrigger":
return "Service Bus Trigger"
case "eventhubtrigger":
return "Event Hub Trigger"
case "eventgridtrigger":
return "Event Grid Trigger"
case "cosmosdbtrigger":
return "Cosmos DB Trigger"
case "kafkatrigger":
return "Kafka Trigger"
default:
name := strings.TrimSuffix(bindingType, "Trigger")
return name + " Trigger"
}
}
func functionTriggerDetail(bindingType string, binding map[string]any, invokeURL string) string {
switch strings.ToLower(bindingType) {
case "httptrigger":
return invokeURL
case "timertrigger":
return mapStringValue(binding, "schedule")
case "blobtrigger":
return mapStringValue(binding, "path")
case "queuetrigger":
return mapStringValue(binding, "queueName")
case "servicebustrigger":
return firstNonEmpty(mapStringValue(binding, "queueName"), mapStringValue(binding, "topicName"))
case "eventhubtrigger":
return mapStringValue(binding, "eventHubName")
case "cosmosdbtrigger":
database := mapStringValue(binding, "databaseName")
container := firstNonEmpty(mapStringValue(binding, "containerName"), mapStringValue(binding, "collectionName"))
return strings.Trim(strings.Join([]string{database, container}, "/"), "/")
case "kafkatrigger":
return firstNonEmpty(mapStringValue(binding, "topic"), mapStringValue(binding, "brokerList"))
default:
return ""
}
}
func mapStringValue(values map[string]any, name string) string {
for key, value := range values {
if strings.EqualFold(key, name) {
if text, ok := value.(string); ok {
return text
}
}
}
return ""
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if value != "" {
return value
}
}
return ""
}
func formatFunctionTestDataLoot(subscriptionID, resourceGroup, appName, functionName, testData string) string {
if testData == "" {
return ""
}
return fmt.Sprintf("az functionapp function show --name %q --resource-group %q --function-name %q --subscription %q --query testData --output tsv\n%s\n\n", appName, resourceGroup, functionName, subscriptionID, testData)
}
func formatFunctionAuthLevel(level string) string {
switch strings.ToLower(level) {
case "anonymous":
return "Anonymous"
case "function":
return "Function key"
case "admin":
return "Admin/master key"
default:
return level
}
}
func uniqueStrings(values []string) []string {
seen := map[string]bool{}
var result []string
for _, value := range values {
if value != "" && !seen[value] {
seen[value] = true
result = append(result, value)
}
}
return result
}
func sortedKeys(values map[string]*string) []string {
keys := make([]string, 0, len(values))
for key := range values {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}
func getFunctionAppsOriginal(subscriptionID string) ([]web.Site, error) {
client := internal.GetWebAppsClient(subscriptionID)
page, err := client.List(context.TODO())
if err != nil {
return nil, err
}
var apps []web.Site
for page.NotDone() {
apps = append(apps, page.Values()...)
if err := page.NextWithContext(context.TODO()); err != nil {
return nil, err
}
}
return apps, nil
}
func getFunctionConfigurationOriginal(subscriptionID, resourceGroup, name string) (functionConfiguration, error) {
client := internal.GetWebAppsClient(subscriptionID)
var config functionConfiguration
settings, err := client.ListApplicationSettings(context.TODO(), resourceGroup, name)
if err != nil {
return config, err
}
config.AppSettings = settings.Properties
connections, err := client.ListConnectionStrings(context.TODO(), resourceGroup, name)
if err != nil {
return config, err
}
config.ConnectionStrings = connections.Properties
authSettings, err := client.GetAuthSettingsV2WithoutSecrets(context.TODO(), resourceGroup, name)
if err != nil {
return config, err
}
config.AuthSettings = authSettings
return config, nil
}
func getFunctionsOriginal(subscriptionID, resourceGroup, appName string) ([]web.FunctionEnvelope, error) {
client := internal.GetWebAppsClient(subscriptionID)
page, err := client.ListFunctions(context.TODO(), resourceGroup, appName)
if err != nil {
return nil, err
}
var functions []web.FunctionEnvelope
for page.NotDone() {
functions = append(functions, page.Values()...)
if err := page.NextWithContext(context.TODO()); err != nil {
return nil, err
}
}
return functions, nil
}
func resourceGroupFromID(id string) string {
parts := strings.Split(strings.Trim(id, "/"), "/")
for i := 0; i+1 < len(parts); i++ {
if strings.EqualFold(parts[i], "resourceGroups") {
return parts[i+1]
}
}
return ""
}
func writeFunctionOutput(outputDirectory, tenantDomain, directorySuffix, prefix string, rows [][]string, loot functionLoot, verbosity int, wrap bool) {
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: globals.AZ_FUNC_MODULE_NAME,
PrefixIdentifier: prefix,
Table: internal.TableClient{
Wrap: wrap,
DirectoryName: filepath.Join(outputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, tenantDomain, directorySuffix),
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{Header: functionTableHeader(), Body: rows, Name: globals.AZ_FUNC_MODULE_NAME, TableCols: functionVisibleColumns()})
o.Loot.DirectoryName = filepath.Join(o.Table.DirectoryName, "loot")
if loot.Configuration != "" {
o.Loot.LootFiles = append(o.Loot.LootFiles, internal.LootFile{Name: "function-app-configuration", Contents: loot.Configuration})
}
if loot.TestData != "" {
o.Loot.LootFiles = append(o.Loot.LootFiles, internal.LootFile{Name: "function-test-data", Contents: loot.TestData})
}
if len(o.Loot.LootFiles) > 0 {
o.WriteFullOutput(o.Table.TableFiles, o.Loot.LootFiles)
} else {
o.WriteFullOutput(o.Table.TableFiles, nil)
}
fmt.Println()
}
+94
View File
@@ -0,0 +1,94 @@
package azure
import (
"errors"
"strings"
"testing"
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/subscriptions"
"github.com/Azure/azure-sdk-for-go/profiles/latest/web/mgmt/web"
"github.com/aws/smithy-go/ptr"
)
func TestGetFunctionDataAndConfigurationLoot(t *testing.T) {
originalApps, originalConfig, originalFunctions := getFunctionApps, getFunctionConfiguration, getFunctions
t.Cleanup(func() {
getFunctionApps, getFunctionConfiguration, getFunctions = originalApps, originalConfig, originalFunctions
})
getFunctionApps = func(string) ([]web.Site, error) {
return []web.Site{
{Name: ptr.String("process-orders"), Kind: ptr.String("functionapp,linux"), ID: ptr.String("/subscriptions/sub/resourceGroups/apps/providers/Microsoft.Web/sites/process-orders"), SiteProperties: &web.SiteProperties{DefaultHostName: ptr.String("process-orders.azurewebsites.net")}},
{Name: ptr.String("ordinary-web-app"), Kind: ptr.String("app")},
}, nil
}
getFunctionConfiguration = func(_, resourceGroup, name string) (functionConfiguration, error) {
if resourceGroup != "apps" || name != "process-orders" {
t.Fatalf("unexpected configuration target %s/%s", resourceGroup, name)
}
return functionConfiguration{AppSettings: map[string]*string{"API_KEY": ptr.String("secret")}, ConnectionStrings: map[string]*web.ConnStringValueTypePair{"Database": {Value: ptr.String("Server=db")}}, AuthSettings: web.SiteAuthSettingsV2{SiteAuthSettingsV2Properties: &web.SiteAuthSettingsV2Properties{Platform: &web.AuthPlatform{Enabled: ptr.Bool(true)}, IdentityProviders: &web.IdentityProviders{AzureActiveDirectory: &web.AzureActiveDirectory{Enabled: ptr.Bool(true)}}}}}, nil
}
getFunctions = func(_, _, _ string) ([]web.FunctionEnvelope, error) {
return []web.FunctionEnvelope{{Name: ptr.String("process-orders/HttpTrigger"), FunctionEnvelopeProperties: &web.FunctionEnvelopeProperties{InvokeURLTemplate: ptr.String("https://process-orders.azurewebsites.net/api/orders"), Config: map[string]any{"bindings": []any{map[string]any{"type": "httpTrigger", "authLevel": "function"}}}, IsDisabled: ptr.Bool(false), TestData: ptr.String(`{"orderId":42}`)}}}, nil
}
sub := subscriptions.Subscription{SubscriptionID: ptr.String("sub"), DisplayName: ptr.String("Production")}
rows, loot, warnings, err := getFunctionData(sub)
if err != nil || len(warnings) != 0 {
t.Fatalf("unexpected errors: err=%v warnings=%v", err, warnings)
}
if len(rows) != 1 || rows[0][0] != "process-orders" || rows[0][1] != "HttpTrigger" || rows[0][2] != "HTTP: https://process-orders.azurewebsites.net/api/orders" || !strings.Contains(rows[0][3], "Function key") || !strings.Contains(rows[0][3], "Entra ID") || rows[0][4] != "Enabled" || rows[0][5] != "Production" || rows[0][6] != "sub" {
t.Fatalf("unexpected function rows: %+v", rows)
}
for _, value := range []string{"API_KEY=secret", "Database=Server=db", "Entra ID"} {
if !strings.Contains(loot.Configuration, value) {
t.Errorf("loot missing %q: %s", value, loot.Configuration)
}
}
for _, value := range []string{"az functionapp function show", `--function-name "HttpTrigger"`, `{"orderId":42}`} {
if !strings.Contains(loot.TestData, value) {
t.Errorf("test-data loot missing %q: %s", value, loot.TestData)
}
}
}
func TestGetFunctionDataFallsBackWhenEnrichmentFails(t *testing.T) {
originalApps, originalConfig, originalFunctions := getFunctionApps, getFunctionConfiguration, getFunctions
t.Cleanup(func() {
getFunctionApps, getFunctionConfiguration, getFunctions = originalApps, originalConfig, originalFunctions
})
getFunctionApps = func(string) ([]web.Site, error) {
return []web.Site{{Name: ptr.String("visible-app"), Kind: ptr.String("functionapp"), ID: ptr.String("/subscriptions/sub/resourceGroups/apps/providers/Microsoft.Web/sites/visible-app"), SiteProperties: &web.SiteProperties{DefaultHostName: ptr.String("visible.azurewebsites.net")}}}, nil
}
getFunctionConfiguration = func(_, _, _ string) (functionConfiguration, error) {
return functionConfiguration{}, errors.New("forbidden")
}
getFunctions = func(_, _, _ string) ([]web.FunctionEnvelope, error) { return nil, nil }
rows, _, warnings, err := getFunctionData(subscriptions.Subscription{SubscriptionID: ptr.String("sub"), DisplayName: ptr.String("Production")})
if err != nil || len(rows) != 1 || rows[0][0] != "visible-app" || rows[0][2] != "App URL: https://visible.azurewebsites.net" || rows[0][4] != "Unknown" || len(warnings) != 1 {
t.Fatalf("expected visible fallback row and warning, got rows=%+v warnings=%v err=%v", rows, warnings, err)
}
}
func TestGetFunctionDataUsesCronScheduleAsTrigger(t *testing.T) {
originalApps, originalConfig, originalFunctions := getFunctionApps, getFunctionConfiguration, getFunctions
t.Cleanup(func() {
getFunctionApps, getFunctionConfiguration, getFunctions = originalApps, originalConfig, originalFunctions
})
getFunctionApps = func(string) ([]web.Site, error) {
return []web.Site{{Name: ptr.String("scheduled-app"), Kind: ptr.String("functionapp"), ID: ptr.String("/subscriptions/sub/resourceGroups/apps/providers/Microsoft.Web/sites/scheduled-app")}}, nil
}
getFunctionConfiguration = func(_, _, _ string) (functionConfiguration, error) { return functionConfiguration{}, nil }
getFunctions = func(_, _, _ string) ([]web.FunctionEnvelope, error) {
return []web.FunctionEnvelope{{
Name: ptr.String("scheduled-app/Cleanup"),
FunctionEnvelopeProperties: &web.FunctionEnvelopeProperties{
Config: map[string]any{"bindings": []any{map[string]any{"type": "timerTrigger", "schedule": "0 */5 * * * *"}}},
IsDisabled: ptr.Bool(true),
},
}}, nil
}
rows, _, _, err := getFunctionData(subscriptions.Subscription{SubscriptionID: ptr.String("sub"), DisplayName: ptr.String("Production")})
if err != nil || len(rows) != 1 || rows[0][2] != "Time Trigger: 0 */5 * * * *" || rows[0][4] != "Disabled" {
t.Fatalf("expected CRON schedule trigger, got rows=%+v err=%v", rows, err)
}
}
+4 -12
View File
@@ -10,8 +10,6 @@ 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 AzInventoryCommand(AzTenantID, AzSubscriptionID, AzOutputDirectory, Version string, AzVerbosity int, AzWrapTable bool, AzMergedTable bool) error {
@@ -40,10 +38,7 @@ func AzInventoryCommand(AzTenantID, AzSubscriptionID, AzOutputDirectory, Version
},
}
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)))
azureLogger.InfoM(fmt.Sprintf("Gathering inventory for tenant %s (%s)", ptr.ToString(tenantInfo.DefaultDomain), ptr.ToString(tenantInfo.ID)), o.CallingModule)
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")
@@ -57,7 +52,7 @@ func AzInventoryCommand(AzTenantID, AzSubscriptionID, AzOutputDirectory, Version
internal.TableFile{
Header: header,
Body: body,
Name: fmt.Sprintf(o.CallingModule)})
Name: o.CallingModule})
if body != nil {
o.WriteFullOutput(o.Table.TableFiles, nil)
@@ -76,7 +71,7 @@ func AzInventoryCommand(AzTenantID, AzSubscriptionID, AzOutputDirectory, Version
} else {
// Error: please make a valid flag selection
fmt.Println("Please enter a valid input with a valid flag. Use --help for info.")
azureLogger.ErrorM("Please enter a valid input with a valid flag. Use --help for info.", globals.AZ_INVENTORY_MODULE_NAME)
}
o.WriteFullOutput(o.Table.TableFiles, nil)
return nil
@@ -101,10 +96,7 @@ func runInventoryCommandForSingleSubscription(AzSubscription string, AzOutputDir
o.PrefixIdentifier = AzSubscriptionInfo.Name
o.Table.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(tenantInfo.DefaultDomain), AzSubscriptionInfo.Name)
fmt.Printf(
"[%s][%s] Gathering inventory for subscription %s\n",
color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)), color.CyanString(o.CallingModule),
fmt.Sprintf("%s (%s)", AzSubscriptionInfo.Name, AzSubscriptionInfo.ID))
azureLogger.InfoM(fmt.Sprintf("Gathering inventory for subscription %s (%s)", AzSubscriptionInfo.Name, AzSubscriptionInfo.ID), o.CallingModule)
// populate the table data
header, body, err = getInventoryInfoPerSubscription(ptr.ToString(tenantInfo.ID), AzSubscriptionInfo.ID)
+22 -31
View File
@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"sort"
@@ -16,7 +15,6 @@ 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 {
@@ -39,19 +37,13 @@ func AzRBACCommand(AzTenantID, AzSubscription, AzOutputFormat, AzOutputDirectory
if AzTenantID != "" && AzSubscription == "" {
// cloudfox azure rbac --tenant [TENANT_ID | PRIMARY_DOMAIN]
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)))
azureLogger.InfoM(fmt.Sprintf("Enumerating RBAC permissions for tenant %s (%s)", ptr.ToString(tenantInfo.DefaultDomain), ptr.ToString(tenantInfo.ID)), globals.AZ_RBAC_MODULE_NAME)
header, body, err = getRBACperTenant(ptr.ToString(tenantInfo.ID), c)
header, body, err := getRBACperTenant(ptr.ToString(tenantInfo.ID), c)
if err != nil {
return err
}
@@ -69,8 +61,7 @@ func AzRBACCommand(AzTenantID, AzSubscription, AzOutputFormat, AzOutputDirectory
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))
azureLogger.InfoM(fmt.Sprintf("Enumerating RBAC permissions for subscription %s (%s)", AzSubscriptionInfo.Name, AzSubscriptionInfo.ID), globals.AZ_RBAC_MODULE_NAME)
header, body = getRBACperSubscription(ptr.ToString(tenantInfo.ID), AzSubscriptionInfo.ID, c)
o.Table.TableFiles = append(o.Table.TableFiles,
internal.TableFile{
@@ -80,7 +71,7 @@ func AzRBACCommand(AzTenantID, AzSubscription, AzOutputFormat, AzOutputDirectory
} else {
// Error: please make a valid flag selection
fmt.Println("Please enter a valid input with a valid flag. Use --help for info.")
azureLogger.ErrorM("Please enter a valid input with a valid flag. Use --help for info.", globals.AZ_RBAC_MODULE_NAME)
}
if body != nil {
@@ -142,13 +133,13 @@ func (c *CloudFoxRBACclient) initialize(tenantID string, subscriptionIDs []strin
for _, subID := range subscriptionIDs {
rd, err := 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)
azureLogger.WarnM(fmt.Sprintf("Failed to get role definitions for subscription %s: %s. Skipping it.", subID, err), globals.AZ_RBAC_MODULE_NAME)
}
c.roleDefinitions = append(c.roleDefinitions, rd...)
ra, err := 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)
azureLogger.WarnM(fmt.Sprintf("Failed to get role assignments for subscription %s: %s. Skipping it.", subID, err), globals.AZ_RBAC_MODULE_NAME)
}
c.roleAssignments = append(c.roleAssignments, ra...)
}
@@ -239,11 +230,11 @@ func mockedGetAzureADUsers(tenantID string) ([]graphrbac.User, error) {
file, err := os.ReadFile(globals.AAD_USERS_TEST_FILE)
if err != nil {
log.Fatalf("could not read file %s", globals.AAD_USERS_TEST_FILE)
azureLogger.FatalM(fmt.Sprintf("Could not read file %s", globals.AAD_USERS_TEST_FILE), globals.AZ_RBAC_MODULE_NAME)
}
err = json.Unmarshal(file, &users)
if err != nil {
log.Fatalf("could not unmarshall file %s", globals.AAD_USERS_TEST_FILE)
azureLogger.FatalM(fmt.Sprintf("Could not unmarshal file %s", globals.AAD_USERS_TEST_FILE), globals.AZ_RBAC_MODULE_NAME)
}
return users.AzureADUsers, nil
}
@@ -253,15 +244,15 @@ func generateAzureADUsersTestFIle(tenantID string) {
// ObjectID *string `json:"objectId,omitempty"`
users, err := getAzureADUsers(tenantID)
if err != nil {
log.Fatalf("could not enumerate users for tenant %s", tenantID)
azureLogger.FatalM(fmt.Sprintf("Could not enumerate users for tenant %s", tenantID), globals.AZ_RBAC_MODULE_NAME)
}
usersJSON, err := json.Marshal(AzureADUsersTestFile{AzureADUsers: users})
if err != nil {
log.Fatalf("could not marshall json for azure ad users in tenant %s", tenantID)
azureLogger.FatalM(fmt.Sprintf("Could not marshal JSON for Azure AD users in tenant %s", tenantID), globals.AZ_RBAC_MODULE_NAME)
}
err = os.WriteFile(globals.AAD_USERS_TEST_FILE, usersJSON, os.ModeAppend)
if err != nil {
log.Fatalf("could not write to azure ad users test file %s", globals.AAD_USERS_TEST_FILE)
azureLogger.FatalM(fmt.Sprintf("Could not write to Azure AD users test file %s", globals.AAD_USERS_TEST_FILE), globals.AZ_RBAC_MODULE_NAME)
}
}
@@ -291,11 +282,11 @@ func mockedGetRoleDefinitions(subscriptionID string) ([]authorization.RoleDefini
var roleDefinitions RoleDefinitionTestFile
file, err := os.ReadFile(globals.ROLE_DEFINITIONS_TEST_FILE)
if err != nil {
log.Fatalf("could not read file %s", globals.ROLE_DEFINITIONS_TEST_FILE)
azureLogger.FatalM(fmt.Sprintf("Could not read file %s", globals.ROLE_DEFINITIONS_TEST_FILE), globals.AZ_RBAC_MODULE_NAME)
}
err = json.Unmarshal(file, &roleDefinitions)
if err != nil {
log.Fatalf("could not unmarshall file %s", globals.ROLE_DEFINITIONS_TEST_FILE)
azureLogger.FatalM(fmt.Sprintf("Could not unmarshal file %s", globals.ROLE_DEFINITIONS_TEST_FILE), globals.AZ_RBAC_MODULE_NAME)
}
return roleDefinitions.RoleDefinitions, nil
}
@@ -307,11 +298,11 @@ func generateRoleDefinitionsTestFile(subscriptionID string) {
roleDefinitions, err := getRoleDefinitions(subscriptionID)
if err != nil {
log.Fatal(err)
azureLogger.FatalM(err.Error(), globals.AZ_RBAC_MODULE_NAME)
}
roleAssignments, err := getRoleAssignments(subscriptionID)
if err != nil {
log.Fatal(err)
azureLogger.FatalM(err.Error(), globals.AZ_RBAC_MODULE_NAME)
}
var roleDefinitionsResults []authorization.RoleDefinition
@@ -333,12 +324,12 @@ func generateRoleDefinitionsTestFile(subscriptionID string) {
rolesjson, err := json.Marshal(tf)
if err != nil {
log.Fatalf("could not marshall json for role definitions in subscription %s", subscriptionID)
azureLogger.FatalM(fmt.Sprintf("Could not marshal JSON for role definitions in subscription %s", subscriptionID), globals.AZ_RBAC_MODULE_NAME)
}
err = os.WriteFile(globals.ROLE_DEFINITIONS_TEST_FILE, rolesjson, os.ModeAppend)
if err != nil {
log.Fatalf("could not write to role definitions test file %s", globals.ROLE_DEFINITIONS_TEST_FILE)
azureLogger.FatalM(fmt.Sprintf("Could not write to role definitions test file %s", globals.ROLE_DEFINITIONS_TEST_FILE), globals.AZ_RBAC_MODULE_NAME)
}
}
@@ -367,11 +358,11 @@ func mockedGetRoleAssignments(subscriptionID string) ([]authorization.RoleAssign
var allRoleAssignments, roleAssignmentsResults []authorization.RoleAssignment
file, err := os.ReadFile(globals.ROLE_ASSIGNMENTS_TEST_FILE)
if err != nil {
log.Fatalf("could not read file %s", globals.ROLE_ASSIGNMENTS_TEST_FILE)
azureLogger.FatalM(fmt.Sprintf("Could not read file %s", globals.ROLE_ASSIGNMENTS_TEST_FILE), globals.AZ_RBAC_MODULE_NAME)
}
err = json.Unmarshal(file, &allRoleAssignments)
if err != nil {
log.Fatalf("could not unmarshall file %s", globals.ROLE_ASSIGNMENTS_TEST_FILE)
azureLogger.FatalM(fmt.Sprintf("Could not unmarshal file %s", globals.ROLE_ASSIGNMENTS_TEST_FILE), globals.AZ_RBAC_MODULE_NAME)
}
for _, ra := range allRoleAssignments {
roleAssignmentSubscriptionID := strings.Split(ptr.ToString(ra.Properties.RoleDefinitionID), "/")[2]
@@ -385,15 +376,15 @@ func mockedGetRoleAssignments(subscriptionID string) ([]authorization.RoleAssign
func generateRoleAssignmentsTestFile(subscriptionID string) {
ra, err := getRoleAssignments(subscriptionID)
if err != nil {
log.Fatalf("could not generate role assignments for subscription %s", subscriptionID)
azureLogger.FatalM(fmt.Sprintf("Could not generate role assignments for subscription %s", subscriptionID), globals.AZ_RBAC_MODULE_NAME)
}
roleAssginments, err := json.Marshal(RoleAssignmentsTestFile{RoleAssignments: ra})
if err != nil {
log.Fatalf("could not marshall json for role assignments in subscription %s", subscriptionID)
azureLogger.FatalM(fmt.Sprintf("Could not marshal JSON for role assignments in subscription %s", subscriptionID), globals.AZ_RBAC_MODULE_NAME)
}
err = os.WriteFile(globals.ROLE_ASSIGNMENTS_TEST_FILE, roleAssginments, os.ModeAppend)
if err != nil {
log.Fatalf("could not write to azure ad users test file %s", globals.ROLE_ASSIGNMENTS_TEST_FILE)
azureLogger.FatalM(fmt.Sprintf("Could not write to role assignments test file %s", globals.ROLE_ASSIGNMENTS_TEST_FILE), globals.AZ_RBAC_MODULE_NAME)
}
}
+2
View File
@@ -9,6 +9,8 @@ import (
"github.com/aws/smithy-go/ptr"
)
var azureLogger = internal.NewLogger()
type TenantInfo struct {
ID *string
DefaultDomain *string
+4 -11
View File
@@ -16,7 +16,6 @@ import (
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/smithy-go/ptr"
"github.com/fatih/color"
"github.com/kyokomi/emoji"
)
// Color functions
@@ -46,9 +45,7 @@ func AzStorageCommand(AzTenantID, AzSubscription, AzOutputFormat, AzOutputDirect
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)))
azureLogger.InfoM(fmt.Sprintf("Enumerating storage accounts for tenant %s (%s)", ptr.ToString(tenantInfo.DefaultDomain), ptr.ToString(tenantInfo.ID)), globals.AZ_STORAGE_MODULE_NAME)
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")
@@ -87,7 +84,7 @@ func AzStorageCommand(AzTenantID, AzSubscription, AzOutputFormat, AzOutputDirect
} else {
// Error: please make a valid flag selection
fmt.Println("Please enter a valid input with a valid flag. Use --help for info.")
azureLogger.ErrorM("Please enter a valid input with a valid flag. Use --help for info.", globals.AZ_STORAGE_MODULE_NAME)
}
return nil
@@ -115,11 +112,7 @@ func runStorageCommandForSingleSubcription(AzSubscription string, AzOutputDirect
o.PrefixIdentifier = AzSubscriptionInfo.Name
o.Table.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(tenantInfo.DefaultDomain), AzSubscriptionInfo.Name)
fmt.Printf(
"[%s][%s] Enumerating storage accounts for subscription %s\n",
color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)),
color.CyanString(globals.AZ_STORAGE_MODULE_NAME),
fmt.Sprintf("%s (%s)", AzSubscriptionInfo.Name, AzSubscriptionInfo.ID))
azureLogger.InfoM(fmt.Sprintf("Enumerating storage accounts for subscription %s (%s)", AzSubscriptionInfo.Name, AzSubscriptionInfo.ID), globals.AZ_STORAGE_MODULE_NAME)
//AzTenantID := ptr.ToString(GetTenantIDPerSubscription(AzSubscription))
header, body, publicBlobURLs, err = getStorageInfoPerSubscription(ptr.ToString(tenantInfo.ID), AzSubscriptionInfo.ID)
if err != nil {
@@ -167,7 +160,7 @@ func writeBlobURLslootFile(callingModule, controlMessagePrefix, outputDirectory
}
}
fmt.Printf("[%s][%s] Loot file written to [%s]\n", cyan(callingModule), cyan(controlMessagePrefix), file.Name())
azureLogger.SuccessM(fmt.Sprintf("Loot file written to [%s]", file.Name()), callingModule)
return nil
}
+68
View File
@@ -0,0 +1,68 @@
package azure
import (
"fmt"
"path/filepath"
"strconv"
"time"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/smithy-go/ptr"
)
func AzTokenCacheCommand(outputDirectory, version string, wrap bool, verbosity int, listResourceGroups bool) error {
azureLogger.InfoM("Enumerating Azure CLI sessions...", globals.AZ_TOKENCACHE_MODULE_NAME)
header, body := getTokenCacheRelevantDataSubsOnly()
fileName := globals.AZ_TOKENCACHE_MODULE_NAME + "-subscriptions"
if listResourceGroups {
header, body = getTokenCacheRelevantDataPerRG()
fileName = globals.AZ_TOKENCACHE_MODULE_NAME + "-resource-groups"
}
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: globals.AZ_TOKENCACHE_MODULE_NAME,
PrefixIdentifier: "N/A",
Table: internal.TableClient{
Wrap: wrap,
DirectoryName: filepath.Join(outputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, "tokencache-data"),
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: header,
Body: body,
Name: fmt.Sprintf("%s-%s", fileName, strconv.FormatInt(time.Now().Unix(), 10)),
})
o.WriteFullOutput(o.Table.TableFiles, nil)
return nil
}
func getTokenCacheRelevantDataPerRG() ([]string, [][]string) {
header := []string{"Tenant ID", "Tenant Primary Domain", "Subscription ID", "Subscription Name", "RG Name", "Region"}
var body [][]string
for _, tenant := range getTenants() {
for _, subscription := range GetSubscriptions() {
if ptr.ToString(tenant.TenantID) != ptr.ToString(subscription.TenantID) {
continue
}
for _, resourceGroup := range getResourceGroups(ptr.ToString(subscription.SubscriptionID)) {
body = append(body, []string{ptr.ToString(subscription.TenantID), ptr.ToString(tenant.DefaultDomain), ptr.ToString(subscription.SubscriptionID), ptr.ToString(subscription.DisplayName), ptr.ToString(resourceGroup.Name), ptr.ToString(resourceGroup.Location)})
}
}
}
return header, body
}
func getTokenCacheRelevantDataSubsOnly() ([]string, [][]string) {
header := []string{"Tenant ID", "Tenant Primary Domain", "Subscription ID", "Subscription Name"}
var body [][]string
for _, tenant := range getTenants() {
for _, subscription := range GetSubscriptions() {
if ptr.ToString(tenant.TenantID) == ptr.ToString(subscription.TenantID) {
body = append(body, []string{ptr.ToString(subscription.TenantID), ptr.ToString(tenant.DefaultDomain), ptr.ToString(subscription.SubscriptionID), ptr.ToString(subscription.DisplayName)})
}
}
}
return header, body
}
+7 -13
View File
@@ -16,8 +16,6 @@ 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 {
@@ -39,9 +37,7 @@ func AzVMsCommand(AzTenantID, AzSubscription, AzOutputFormat, AzOutputDirectory,
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)))
azureLogger.InfoM(fmt.Sprintf("Enumerating VMs for tenant %s (%s)", ptr.ToString(tenantInfo.DefaultDomain), ptr.ToString(tenantInfo.ID)), globals.AZ_VMS_MODULE_NAME)
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")
@@ -84,7 +80,7 @@ func AzVMsCommand(AzTenantID, AzSubscription, AzOutputFormat, AzOutputDirectory,
} else {
// Error: please make a valid flag selection
fmt.Println("Please enter a valid input with a valid flag. Use --help for info.")
azureLogger.ErrorM("Please enter a valid input with a valid flag. Use --help for info.", globals.AZ_VMS_MODULE_NAME)
}
return nil
@@ -110,9 +106,7 @@ func runVMsCommandForSingleSubscription(AzSubscription string, AzOutputDirectory
o.PrefixIdentifier = AzSubscriptionInfo.Name
o.Table.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(tenantInfo.DefaultDomain), AzSubscriptionInfo.Name)
fmt.Printf("[%s][%s] Enumerating VMs for subscription %s\n",
color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)), color.CyanString(globals.AZ_VMS_MODULE_NAME),
fmt.Sprintf("%s (%s)", AzSubscriptionInfo.Name, AzSubscriptionInfo.ID))
azureLogger.InfoM(fmt.Sprintf("Enumerating VMs for subscription %s (%s)", AzSubscriptionInfo.Name, AzSubscriptionInfo.ID), globals.AZ_VMS_MODULE_NAME)
// populate the table data
header, body, userData = getVMsPerSubscriptionID(AzSubscriptionInfo.ID)
@@ -153,7 +147,7 @@ func getVMsPerTenantID(AzTenantID string) ([]string, [][]string, string) {
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))
azureLogger.WarnM(fmt.Sprintf("Could not enumerate VMs for resource group %s in subscription %s: %s", ptr.ToString(rg.Name), ptr.ToString(s.SubscriptionID), err), globals.AZ_VMS_MODULE_NAME)
} else {
resultsBody = append(resultsBody, b...)
userDataCombined += userData
@@ -175,7 +169,7 @@ func getVMsPerSubscriptionID(AzSubscriptionID string) ([]string, [][]string, str
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))
azureLogger.WarnM(fmt.Sprintf("Could not enumerate VMs for resource group %s in subscription %s: %s", ptr.ToString(rg.Name), ptr.ToString(s.SubscriptionID), err), globals.AZ_VMS_MODULE_NAME)
} else {
resultsBody = append(resultsBody, b...)
userDataCombined += userData
@@ -209,13 +203,13 @@ func getComputeRelevantData(sub subscriptions.Subscription, rg resources.Group)
// get userdata
vmDetails, err := getComputeVmInfo(subscriptionID, resourceGroupName, ptr.ToString(vm.Name))
if err != nil {
fmt.Println("error fetching vm details for vm: ", ptr.ToString(vm.Name))
azureLogger.WarnM(fmt.Sprintf("Error fetching VM details for %s: %s", ptr.ToString(vm.Name), err), globals.AZ_VMS_MODULE_NAME)
}
if vmDetails.VirtualMachineProperties != nil && vmDetails.VirtualMachineProperties.UserData != nil {
userData, err := base64.StdEncoding.DecodeString(ptr.ToString(vmDetails.VirtualMachineProperties.UserData))
if err != nil {
fmt.Println("error decoding userdata for vm: ", ptr.ToString(vm.Name))
azureLogger.WarnM(fmt.Sprintf("Error decoding user data for VM %s: %s", ptr.ToString(vm.Name), err), globals.AZ_VMS_MODULE_NAME)
}
//append userdata from this vm to the string with headers and newlines for VM name, location, and resource group name
userDataString += fmt.Sprintf(
+1 -1
View File
@@ -92,7 +92,7 @@ func TestAzVMsCommand(t *testing.T) {
err := AzVMsCommand(s.azTenantID, s.azSubscriptionID, s.azOutputFormat, s.azOutputDirectory, s.version, 2, s.wrapTableOutput, s.azMergedTable)
if err != nil {
log.Fatalf(err.Error())
log.Fatal(err)
}
}
}
+334 -69
View File
@@ -2,56 +2,66 @@ package azure
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"log"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/profiles/latest/authorization/mgmt/authorization"
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/resources"
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/subscriptions"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"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 {
func AzWhoamiCommand(AzOutputDirectory, version string, AzWrapTable bool, AzVerbosity int) error {
o := internal.OutputClient{
Verbosity: AzVerbosity,
CallingModule: globals.AZ_WHOAMI_MODULE_NAME,
Table: internal.TableClient{
Wrap: AzWrapTable,
Wrap: AzWrapTable,
DirectoryName: filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, "whoami-data"),
},
}
fmt.Printf("[%s][%s] Enumerating Azure CLI sessions...\n", color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", version)), color.CyanString(globals.AZ_WHOAMI_MODULE_NAME))
var header []string
var body [][]string
fileTimestamp := strconv.FormatInt(time.Now().Unix(), 10)
o.PrefixIdentifier = "N/A"
if !AzWhoamiListRGsAlso {
// cloudfox azure whoami
header, body = getWhoamiRelevantDataSubsOnly()
o.Table.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, "whoami-data")
// append timestamp to filename (time from epoch)
o.Table.TableFiles = append(o.Table.TableFiles,
internal.TableFile{
Header: header,
Body: body,
Name: fmt.Sprintf(globals.AZ_WHOAMI_MODULE_NAME+"-subs-only") + "-" + strconv.FormatInt((time.Now().Unix()), 10)})
azureLogger.InfoM("Enumerating caller's Entra ID roles", globals.AZ_WHOAMI_MODULE_NAME)
roleHeader, roleBody, roleErrors := getWhoamiEntraRoles()
for _, err := range roleErrors {
azureLogger.WarnM(fmt.Sprintf("Could not enumerate Entra ID roles: %s", err), globals.AZ_WHOAMI_MODULE_NAME)
}
if len(roleBody) > 0 {
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: roleHeader,
Body: roleBody,
Name: globals.AZ_WHOAMI_MODULE_NAME + "-entra-memberships-" + fileTimestamp,
})
}
} else {
// cloudfox azure whoami --list-rgs
header, body = getWhoamiRelevantDataPerRG()
o.Table.DirectoryName = filepath.Join(ptr.ToString(internal.GetLogDirPath()), globals.AZ_DIR_BASE, "whoami-data")
o.Table.TableFiles = append(o.Table.TableFiles,
internal.TableFile{
Header: header,
Body: body,
Name: globals.AZ_WHOAMI_MODULE_NAME + "-" + strconv.FormatInt((time.Now().Unix()), 10)})
azureLogger.InfoM("Enumerating caller's Azure RBAC roles", globals.AZ_WHOAMI_MODULE_NAME)
rbacHeader, rbacBody, rbacErrors := getWhoamiAzureRBAC()
for _, err := range rbacErrors {
azureLogger.WarnM(fmt.Sprintf("Could not enumerate Azure RBAC roles: %s", err), globals.AZ_WHOAMI_MODULE_NAME)
}
if len(rbacBody) > 0 {
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: rbacHeader,
Body: rbacBody,
Name: globals.AZ_WHOAMI_MODULE_NAME + "-azure-rbac-" + fileTimestamp,
})
}
//internal.PrintTableToScreen(header, body, AzWrapTable)
@@ -60,52 +70,307 @@ func AzWhoamiCommand(AzOutputDirectory, version string, AzWrapTable bool, AzVerb
return nil
}
func getWhoamiRelevantDataPerRG() ([]string, [][]string) {
tableHead := []string{"Tenant ID", "Tentant Primary Domain", "Subscription ID", "Subscription Name", "RG Name", "Region"}
var tableBody [][]string
const microsoftGraphScope = "https://graph.microsoft.com/.default"
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)) {
tableBody = append(
tableBody,
[]string{
ptr.ToString(s.TenantID),
ptr.ToString(t.DefaultDomain),
ptr.ToString(s.SubscriptionID),
ptr.ToString(s.DisplayName),
ptr.ToString(rg.Name),
ptr.ToString(rg.Location),
})
}
}
}
}
var (
graphBaseURL = "https://graph.microsoft.com/v1.0"
graphHTTPClient = http.DefaultClient
getWhoamiEntraRoles = getWhoamiEntraRolesOriginal
getWhoamiAzureRBAC = getWhoamiAzureRBACOriginal
)
return tableHead, tableBody
type entraRole struct {
ODataType string `json:"@odata.type"`
ID string `json:"id"`
DisplayName string `json:"displayName"`
Description string `json:"description"`
}
func getWhoamiRelevantDataSubsOnly() ([]string, [][]string) {
tableHead := []string{"Tenant ID", "Tenant Primary Domain", "Subscription ID", "Subscription Name"}
var tableBody [][]string
type entraRolePage struct {
Value []entraRole `json:"value"`
NextLink string `json:"@odata.nextLink"`
}
for _, t := range getTenants() {
for _, s := range GetSubscriptions() {
if ptr.ToString(t.TenantID) == ptr.ToString(s.TenantID) {
tableBody = append(
tableBody,
[]string{
ptr.ToString(s.TenantID),
ptr.ToString(t.DefaultDomain),
ptr.ToString(s.SubscriptionID),
ptr.ToString(s.DisplayName),
})
type graphErrorResponse struct {
Error struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
type tokenClaims struct {
ObjectID string `json:"oid"`
Name string `json:"name"`
PreferredUsername string `json:"preferred_username"`
AppID string `json:"appid"`
IdentityType string `json:"idtyp"`
}
func getWhoamiEntraRolesOriginal() ([]string, [][]string, []error) {
header := []string{"Tenant ID", "Tenant Primary Domain", "Principal ID", "Principal Name", "Principal Type", "Membership Type", "Membership Name", "Description"}
var body [][]string
var errs []error
for _, tenant := range getTenants() {
tenantID := ptr.ToString(tenant.TenantID)
cred, err := azidentity.NewAzureCLICredential(&azidentity.AzureCLICredentialOptions{TenantID: tenantID})
if err != nil {
errs = append(errs, fmt.Errorf("tenant %s: create Azure CLI credential: %w", tenantID, err))
continue
}
claims, memberships, err := getCallerDirectoryMemberships(context.Background(), cred)
if err != nil {
errs = append(errs, fmt.Errorf("tenant %s: %w", tenantID, err))
continue
}
principalName := claims.PreferredUsername
if principalName == "" {
principalName = claims.Name
}
principalType := "User"
if claims.IdentityType == "app" || (claims.PreferredUsername == "" && claims.AppID != "") {
principalType = "Service Principal"
}
foundRole := false
for _, membership := range memberships {
membershipType := directoryMembershipType(membership.ODataType)
if membershipType == "" {
continue
}
membershipName := membership.DisplayName
if membershipName == "" {
membershipName = membership.ID
}
body = append(body, []string{tenantID, ptr.ToString(tenant.DefaultDomain), claims.ObjectID, principalName, principalType, membershipType, membershipName, membership.Description})
foundRole = foundRole || membershipType == "Directory Role"
}
if !foundRole {
body = append(body, []string{tenantID, ptr.ToString(tenant.DefaultDomain), claims.ObjectID, principalName, principalType, "Directory Role", "(none)", "No active Entra ID directory roles found"})
}
}
return tableHead, tableBody
sort.Slice(body, func(i, j int) bool {
if body[i][0] == body[j][0] {
if body[i][5] == body[j][5] {
return body[i][6] < body[j][6]
}
return body[i][5] < body[j][5]
}
return body[i][0] < body[j][0]
})
return header, body, errs
}
func getCallerEntraRoles(ctx context.Context, cred azcore.TokenCredential) (tokenClaims, []entraRole, error) {
claims, memberships, err := getCallerDirectoryMemberships(ctx, cred)
if err != nil {
return tokenClaims{}, nil, err
}
var roles []entraRole
for _, object := range memberships {
if directoryMembershipType(object.ODataType) == "Directory Role" {
roles = append(roles, object)
}
}
return claims, roles, nil
}
func directoryMembershipType(odataType string) string {
switch {
case strings.EqualFold(odataType, "#microsoft.graph.directoryRole"), strings.EqualFold(odataType, "#Microsoft.DirectoryServices.DirectoryRole"):
return "Directory Role"
case strings.EqualFold(odataType, "#microsoft.graph.group"), strings.EqualFold(odataType, "#Microsoft.DirectoryServices.Group"):
return "Group"
default:
return ""
}
}
func getCallerDirectoryMemberships(ctx context.Context, cred azcore.TokenCredential) (tokenClaims, []entraRole, error) {
token, err := cred.GetToken(ctx, policy.TokenRequestOptions{Scopes: []string{microsoftGraphScope}})
if err != nil {
return tokenClaims{}, nil, fmt.Errorf("get Microsoft Graph access token: %w", err)
}
claims, err := parseTokenClaims(token.Token)
if err != nil {
return tokenClaims{}, nil, err
}
nextLink := fmt.Sprintf("%s/directoryObjects/%s/transitiveMemberOf", strings.TrimRight(graphBaseURL, "/"), url.PathEscape(claims.ObjectID))
var memberships []entraRole
for nextLink != "" {
page, err := getEntraRolePage(ctx, token.Token, nextLink)
if err != nil {
return tokenClaims{}, nil, err
}
memberships = append(memberships, page.Value...)
nextLink = page.NextLink
}
return claims, memberships, nil
}
type rbacPrincipal struct {
ID string
Name string
AssignmentType string
}
func getWhoamiAzureRBACOriginal() ([]string, [][]string, []error) {
header := []string{"Tenant ID", "Subscription ID", "Subscription Name", "Principal ID", "Principal Name", "Assignment Via", "Azure RBAC Role", "Scope"}
var body [][]string
var errs []error
for _, tenant := range getTenants() {
tenantID := ptr.ToString(tenant.TenantID)
cred, err := azidentity.NewAzureCLICredential(&azidentity.AzureCLICredentialOptions{TenantID: tenantID})
if err != nil {
errs = append(errs, fmt.Errorf("tenant %s: create Azure CLI credential: %w", tenantID, err))
continue
}
claims, memberships, err := getCallerDirectoryMemberships(context.Background(), cred)
if err != nil {
errs = append(errs, fmt.Errorf("tenant %s: get caller's groups: %w", tenantID, err))
continue
}
callerName := claims.PreferredUsername
if callerName == "" {
callerName = claims.Name
}
principals := map[string]rbacPrincipal{
strings.ToLower(claims.ObjectID): {ID: claims.ObjectID, Name: callerName, AssignmentType: "Direct"},
}
for _, object := range memberships {
if directoryMembershipType(object.ODataType) != "Group" {
continue
}
groupName := object.DisplayName
if groupName == "" {
groupName = object.ID
}
principals[strings.ToLower(object.ID)] = rbacPrincipal{ID: object.ID, Name: groupName, AssignmentType: "Group"}
}
foundAssignment := false
for _, subscription := range GetSubscriptionsPerTenantID(tenantID) {
subscriptionID := ptr.ToString(subscription.SubscriptionID)
assignments, err := getRoleAssignments(subscriptionID)
if err != nil {
errs = append(errs, fmt.Errorf("subscription %s: %w", subscriptionID, err))
continue
}
definitions, err := getRoleDefinitions(subscriptionID)
if err != nil {
errs = append(errs, fmt.Errorf("subscription %s: %w", subscriptionID, err))
}
roleNames := roleDefinitionNames(definitions)
rows := azureRBACRowsForSubscription(tenantID, subscriptionID, ptr.ToString(subscription.DisplayName), principals, roleNames, assignments)
body = append(body, rows...)
foundAssignment = foundAssignment || len(rows) > 0
}
if !foundAssignment {
body = append(body, []string{tenantID, "", "", claims.ObjectID, callerName, "Direct or Group", "(none)", "No Azure RBAC assignments found in accessible subscriptions"})
}
}
sort.Slice(body, func(i, j int) bool {
for _, column := range []int{0, 1, 6, 5, 4} {
if body[i][column] != body[j][column] {
return body[i][column] < body[j][column]
}
}
return body[i][7] < body[j][7]
})
return header, body, errs
}
func azureRBACRowsForSubscription(tenantID, subscriptionID, subscriptionName string, principals map[string]rbacPrincipal, roleNames map[string]string, assignments []authorization.RoleAssignment) [][]string {
var rows [][]string
for _, assignment := range assignments {
principal, ok := principals[strings.ToLower(ptr.ToString(assignment.Properties.PrincipalID))]
if !ok {
continue
}
roleDefinitionID := lastResourceIDSegment(ptr.ToString(assignment.Properties.RoleDefinitionID))
roleName := roleNames[strings.ToLower(roleDefinitionID)]
if roleName == "" {
roleName = roleDefinitionID
}
rows = append(rows, []string{tenantID, subscriptionID, subscriptionName, principal.ID, principal.Name, principal.AssignmentType, roleName, ptr.ToString(assignment.Properties.Scope)})
}
return rows
}
func roleDefinitionNames(definitions []authorization.RoleDefinition) map[string]string {
names := make(map[string]string, len(definitions))
for _, definition := range definitions {
names[strings.ToLower(lastResourceIDSegment(ptr.ToString(definition.ID)))] = ptr.ToString(definition.RoleName)
}
return names
}
func lastResourceIDSegment(id string) string {
id = strings.TrimRight(id, "/")
if index := strings.LastIndex(id, "/"); index >= 0 {
return id[index+1:]
}
return id
}
func parseTokenClaims(token string) (tokenClaims, error) {
parts := strings.Split(token, ".")
if len(parts) != 3 {
return tokenClaims{}, fmt.Errorf("Microsoft Graph returned a malformed access token")
}
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return tokenClaims{}, fmt.Errorf("decode Microsoft Graph access token claims: %w", err)
}
var claims tokenClaims
if err := json.Unmarshal(payload, &claims); err != nil {
return tokenClaims{}, fmt.Errorf("decode Microsoft Graph access token claims: %w", err)
}
if claims.ObjectID == "" {
return tokenClaims{}, fmt.Errorf("Microsoft Graph access token has no oid claim")
}
return claims, nil
}
func getEntraRolePage(ctx context.Context, accessToken, endpoint string) (entraRolePage, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return entraRolePage{}, fmt.Errorf("create Microsoft Graph request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("Accept", "application/json")
req.Header.Set("ConsistencyLevel", "eventual")
req.Header.Set("User-Agent", globals.CLOUDFOX_USER_AGENT)
resp, err := graphHTTPClient.Do(req)
if err != nil {
return entraRolePage{}, fmt.Errorf("query Microsoft Graph: %w", err)
}
defer resp.Body.Close()
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
return entraRolePage{}, fmt.Errorf("read Microsoft Graph response: %w", err)
}
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
var graphErr graphErrorResponse
if json.Unmarshal(responseBody, &graphErr) == nil && graphErr.Error.Message != "" {
return entraRolePage{}, fmt.Errorf("Microsoft Graph returned %s (%s): %s", resp.Status, graphErr.Error.Code, graphErr.Error.Message)
}
return entraRolePage{}, fmt.Errorf("Microsoft Graph returned %s", resp.Status)
}
var page entraRolePage
if err := json.Unmarshal(responseBody, &page); err != nil {
return entraRolePage{}, fmt.Errorf("decode Microsoft Graph response: %w", err)
}
return page, nil
}
var getTenants = getTenantsOriginal
@@ -115,7 +380,7 @@ func getTenantsOriginal() []subscriptions.TenantIDDescription {
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")
azureLogger.FatalM("Could not get tenants for active session", globals.AZ_WHOAMI_MODULE_NAME)
}
results = append(results, page.Values()...)
}
@@ -141,7 +406,7 @@ func getSubscriptionsOriginal() []subscriptions.Subscription {
subsClient := internal.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")
azureLogger.FatalM("Could not get subscriptions for active session", globals.AZ_WHOAMI_MODULE_NAME)
}
results = append(results, page.Values()...)
}
@@ -171,7 +436,7 @@ func getResourceGroupsOriginal(subscriptionID string) []resources.Group {
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)
azureLogger.FatalM(fmt.Sprintf("Error reading resource groups for subscription %s", subscriptionID), globals.AZ_WHOAMI_MODULE_NAME)
}
results = append(results, page.Values()...)
}
@@ -199,12 +464,12 @@ func mockedGetResourceGroups(subscriptionID string) []resources.Group {
func loadTestFile(fileName string) ResourcesTestFile {
file, err := os.ReadFile(fileName)
if err != nil {
log.Fatalf("could not read file %s", globals.RESOURCES_TEST_FILE)
azureLogger.FatalM(fmt.Sprintf("Could not read file %s", globals.RESOURCES_TEST_FILE), globals.AZ_WHOAMI_MODULE_NAME)
}
var testFile ResourcesTestFile
err = json.Unmarshal(file, &testFile)
if err != nil {
log.Fatalf("could not unmarshall file %s", globals.RESOURCES_TEST_FILE)
azureLogger.FatalM(fmt.Sprintf("Could not unmarshal file %s", globals.RESOURCES_TEST_FILE), globals.AZ_WHOAMI_MODULE_NAME)
}
return testFile
}
+131
View File
@@ -0,0 +1,131 @@
package azure
import (
"context"
"encoding/base64"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/Azure/azure-sdk-for-go/profiles/latest/authorization/mgmt/authorization"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
)
type staticTokenCredential struct {
token string
err error
scope string
}
func TestAzureRBACRowsIncludeDirectAndGroupAssignments(t *testing.T) {
var assignments []authorization.RoleAssignment
err := json.Unmarshal([]byte(`[
{"properties":{"principalId":"caller-id","roleDefinitionId":"/providers/Microsoft.Authorization/roleDefinitions/reader-id","scope":"/subscriptions/sub-id"}},
{"properties":{"principalId":"group-id","roleDefinitionId":"/providers/Microsoft.Authorization/roleDefinitions/owner-id","scope":"/subscriptions/sub-id/resourceGroups/example"}},
{"properties":{"principalId":"other-id","roleDefinitionId":"/providers/Microsoft.Authorization/roleDefinitions/ignored-id","scope":"/subscriptions/sub-id"}}
]`), &assignments)
if err != nil {
t.Fatalf("unmarshal role assignments: %v", err)
}
principals := map[string]rbacPrincipal{
"caller-id": {ID: "caller-id", Name: "[email protected]", AssignmentType: "Direct"},
"group-id": {ID: "group-id", Name: "Cloud Operators", AssignmentType: "Group"},
}
roleNames := map[string]string{"reader-id": "Reader", "owner-id": "Owner"}
rows := azureRBACRowsForSubscription("tenant-id", "sub-id", "Production", principals, roleNames, assignments)
if len(rows) != 2 {
t.Fatalf("got %d relevant role assignments, want 2: %+v", len(rows), rows)
}
if rows[0][5] != "Direct" || rows[0][6] != "Reader" {
t.Fatalf("unexpected direct assignment row: %+v", rows[0])
}
if rows[1][4] != "Cloud Operators" || rows[1][5] != "Group" || rows[1][6] != "Owner" {
t.Fatalf("unexpected group assignment row: %+v", rows[1])
}
}
func (c *staticTokenCredential) GetToken(_ context.Context, options policy.TokenRequestOptions) (azcore.AccessToken, error) {
if len(options.Scopes) > 0 {
c.scope = options.Scopes[0]
}
return azcore.AccessToken{Token: c.token, ExpiresOn: time.Now().Add(time.Hour)}, c.err
}
func unsignedTestToken(payload string) string {
return "header." + base64.RawURLEncoding.EncodeToString([]byte(payload)) + ".signature"
}
func TestGetCallerEntraRoles(t *testing.T) {
var authorization string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authorization = r.Header.Get("Authorization")
if r.URL.Path != "/directoryObjects/caller-object-id/transitiveMemberOf" {
t.Fatalf("unexpected Graph path: %s", r.URL.Path)
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"value":[{"@odata.type":"#Microsoft.DirectoryServices.Group","id":"group-id","displayName":"Readers"},{"@odata.type":"#Microsoft.DirectoryServices.DirectoryRole","id":"role-id","displayName":"Global Reader","description":"Read everything"}]}`))
}))
defer server.Close()
originalBaseURL, originalHTTPClient := graphBaseURL, graphHTTPClient
graphBaseURL, graphHTTPClient = server.URL, server.Client()
t.Cleanup(func() {
graphBaseURL, graphHTTPClient = originalBaseURL, originalHTTPClient
})
cred := &staticTokenCredential{token: unsignedTestToken(`{"oid":"caller-object-id","preferred_username":"[email protected]"}`)}
claims, roles, err := getCallerEntraRoles(context.Background(), cred)
if err != nil {
t.Fatalf("getCallerEntraRoles returned an error: %v", err)
}
if cred.scope != microsoftGraphScope {
t.Fatalf("requested scope %q, want %q", cred.scope, microsoftGraphScope)
}
if authorization != "Bearer "+cred.token {
t.Fatalf("authorization header %q does not contain the Graph token", authorization)
}
if claims.ObjectID != "caller-object-id" || len(roles) != 1 || roles[0].DisplayName != "Global Reader" {
t.Fatalf("unexpected claims or roles: claims=%+v roles=%+v", claims, roles)
}
}
func TestDirectoryMembershipTypeSupportsGraphAndLegacyNames(t *testing.T) {
tests := map[string]string{
"#microsoft.graph.directoryRole": "Directory Role",
"#Microsoft.DirectoryServices.DirectoryRole": "Directory Role",
"#microsoft.graph.group": "Group",
"#Microsoft.DirectoryServices.Group": "Group",
"#Microsoft.DirectoryServices.AdministrativeUnit": "",
}
for odataType, want := range tests {
if got := directoryMembershipType(odataType); got != want {
t.Errorf("directoryMembershipType(%q) = %q, want %q", odataType, got, want)
}
}
}
func TestGetCallerEntraRolesGraphError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusForbidden)
_, _ = w.Write([]byte(`{"error":{"code":"Authorization_RequestDenied","message":"Insufficient privileges"}}`))
}))
defer server.Close()
originalBaseURL, originalHTTPClient := graphBaseURL, graphHTTPClient
graphBaseURL, graphHTTPClient = server.URL, server.Client()
t.Cleanup(func() {
graphBaseURL, graphHTTPClient = originalBaseURL, originalHTTPClient
})
cred := &staticTokenCredential{token: unsignedTestToken(`{"oid":"caller-object-id"}`)}
_, _, err := getCallerEntraRoles(context.Background(), cred)
if err == nil || !strings.Contains(err.Error(), "Insufficient privileges") {
t.Fatalf("expected a descriptive Graph permission error, got %v", err)
}
}
+29 -5
View File
@@ -9,14 +9,19 @@ import (
"github.com/aws/smithy-go/ptr"
)
func TestAzWhoamiCommand(t *testing.T) {
func TestAzTokenCacheCommand(t *testing.T) {
fmt.Println()
fmt.Println("[test case] Azure Whoami Command")
fmt.Println("[test case] Azure Token Cache Command")
// Mocked functions to simulate Azure calls and responses
getTenants = mockedGetTenants
GetSubscriptions = mockedGetSubscriptions
getResourceGroups = mockedGetResourceGroups
t.Cleanup(func() {
getTenants = getTenantsOriginal
GetSubscriptions = getSubscriptionsOriginal
getResourceGroups = getResourceGroupsOriginal
})
// Test case parameters
internal.MockFileSystem(true)
@@ -29,7 +34,7 @@ func TestAzWhoamiCommand(t *testing.T) {
azOutputDirectory string
}{
{
name: "./cloudfox azure whoami",
name: "./cloudfox azure tokencache",
resourcesTestFile: "./test-data/resources.json",
azExtendedFilter: false,
version: "DEV",
@@ -37,7 +42,7 @@ func TestAzWhoamiCommand(t *testing.T) {
azOutputDirectory: "~/.cloudfox",
},
{
name: "./cloudfox azure whoami --extended",
name: "./cloudfox azure tokencache --list-rgs",
resourcesTestFile: "./test-data/resources.json",
azExtendedFilter: true,
version: "DEV",
@@ -49,7 +54,26 @@ func TestAzWhoamiCommand(t *testing.T) {
globals.RESOURCES_TEST_FILE = s.resourcesTestFile
fmt.Println()
fmt.Printf("[subtest] %s\n", s.name)
AzWhoamiCommand(s.azOutputDirectory, s.version, s.wrapTableOutput, 1, false)
if err := AzTokenCacheCommand(s.azOutputDirectory, s.version, s.wrapTableOutput, 1, s.azExtendedFilter); err != nil {
t.Fatal(err)
}
}
}
func TestAzWhoamiCommand(t *testing.T) {
originalEntra, originalRBAC := getWhoamiEntraRoles, getWhoamiAzureRBAC
getWhoamiEntraRoles = func() ([]string, [][]string, []error) {
return []string{"Tenant ID", "Membership"}, [][]string{{"tenant", "Global Reader"}}, nil
}
getWhoamiAzureRBAC = func() ([]string, [][]string, []error) {
return []string{"Tenant ID", "Azure RBAC Role"}, [][]string{{"tenant", "Reader"}}, nil
}
t.Cleanup(func() {
getWhoamiEntraRoles = originalEntra
getWhoamiAzureRBAC = originalRBAC
})
if err := AzWhoamiCommand("~/.cloudfox", "DEV", false, 1); err != nil {
t.Fatal(err)
}
}
+7 -2
View File
@@ -92,8 +92,9 @@ var (
AWSUseCache bool
AWSMFAToken string
Goroutines int
Verbosity int
Goroutines int
Verbosity int
AWSMaxRetries int
AWSCommands = &cobra.Command{
Use: "aws",
@@ -546,6 +547,9 @@ type OrgAccounts struct {
func awsPreRun(cmd *cobra.Command, args []string) {
gob.Register(&types.Organization{})
// Set the max retries before any AWS config loading happens
internal.MaxRetries = AWSMaxRetries
// if multiple profiles were used, ensure the management account is first
// if AWSProfilesList != "" || AWSAllProfiles {
// AWSProfiles = FindOrgMgmtAccountAndReorderAccounts(AWSProfiles, cmd.Root().Version, AWSMFAToken)
@@ -2553,6 +2557,7 @@ func init() {
AWSCommands.PersistentFlags().BoolVarP(&AWSUseCache, "cached", "c", false, "Load cached data from disk. Faster, but if changes have been recently made you'll miss them")
AWSCommands.PersistentFlags().StringVarP(&AWSTableCols, "cols", "t", "", "Comma separated list of columns to display in table output")
AWSCommands.PersistentFlags().StringVar(&AWSMFAToken, "mfa-token", "", "MFA Token")
AWSCommands.PersistentFlags().IntVar(&AWSMaxRetries, "max-retries", 3, "Maximum number of AWS API retry attempts. Set to 0 for no retries (faster for black-box/limited-permission enumeration)")
AWSCommands.PersistentFlags().StringVar(&PmapperDataBasePath, "pmapper-data-basepath", "", "Supply the base path for the pmapper data files (useful if you have copied them from another machine)\nPoint to the parent directory that contains all of the pmapper data by account numbers. \n\tExample: /path/to/com.nccgroup.principalmapper/\n\tExample: ./pmapperdata/")
AWSCommands.AddCommand(
+71 -12
View File
@@ -1,12 +1,13 @@
package cli
import (
"log"
"github.com/BishopFox/cloudfox/azure"
"github.com/BishopFox/cloudfox/internal"
"github.com/spf13/cobra"
)
var azureLogger = internal.NewLogger()
var (
AzTenantID string
AzSubscription string
@@ -26,18 +27,34 @@ var (
cmd.Help()
},
}
AzWhoamiListRGsAlso bool
AzTokenCacheListRGs bool
AzWhoamiCommand = &cobra.Command{
Use: "whoami",
Aliases: []string{},
Short: "Display available Azure CLI sessions",
Short: "Display the caller's Entra ID memberships and Azure RBAC assignments",
Long: `
Display Available Azure CLI Sessions:
Display the caller's Entra ID memberships and Azure RBAC assignments:
./cloudfox az whoami`,
Run: func(cmd *cobra.Command, args []string) {
err := azure.AzWhoamiCommand(AzOutputDirectory, cmd.Root().Version, AzWrapTable, AzVerbosity, AzWhoamiListRGsAlso)
err := azure.AzWhoamiCommand(AzOutputDirectory, cmd.Root().Version, AzWrapTable, AzVerbosity)
if err != nil {
log.Fatal(err)
azureLogger.FatalM(err.Error(), "whoami")
}
},
}
AzTokenCacheCommand = &cobra.Command{
Use: "tokencache",
Aliases: []string{"token-cache"},
Short: "Display available Azure CLI sessions",
Long: `
Display available Azure CLI tenants and subscriptions:
./cloudfox az tokencache
Include resource groups:
./cloudfox az tokencache --list-rgs`,
Run: func(cmd *cobra.Command, args []string) {
if err := azure.AzTokenCacheCommand(AzOutputDirectory, cmd.Root().Version, AzWrapTable, AzVerbosity, AzTokenCacheListRGs); err != nil {
azureLogger.FatalM(err.Error(), "tokencache")
}
},
}
@@ -55,7 +72,7 @@ Enumerate inventory for a specific subscription:
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)
azureLogger.FatalM(err.Error(), "inventory")
}
},
}
@@ -74,7 +91,7 @@ Enumerate role assignments for a specific subscription:
err := azure.AzRBACCommand(AzTenantID, AzSubscription, AzOutputFormat, AzOutputDirectory, cmd.Root().Version, AzVerbosity, AzWrapTable, AzMergedTable)
if err != nil {
log.Fatal(err)
azureLogger.FatalM(err.Error(), "rbac")
}
},
}
@@ -91,7 +108,7 @@ Enumerate VMs for a specific subscription:
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)
azureLogger.FatalM(err.Error(), "vms")
}
},
}
@@ -109,7 +126,45 @@ Enumerate storage accounts for a specific subscription:
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)
azureLogger.FatalM(err.Error(), "storage")
}
},
}
AzACRCommand = &cobra.Command{
Use: "acr",
Aliases: []string{"container-registries"},
Short: "Enumerates Azure Container Registries",
Long: `
Enumerate container registries for a specific tenant:
./cloudfox az acr --tenant TENANT_ID
Enumerate container registries for a specific subscription:
./cloudfox az acr --subscription SUBSCRIPTION_ID
`,
Run: func(cmd *cobra.Command, args []string) {
err := azure.AzACRCommand(AzTenantID, AzSubscription, AzOutputDirectory, cmd.Root().Version, AzVerbosity, AzWrapTable, AzMergedTable)
if err != nil {
azureLogger.FatalM(err.Error(), "acr")
}
},
}
AzFuncCommand = &cobra.Command{
Use: "func",
Aliases: []string{"functions"},
Short: "Enumerates Azure Function Apps and their configuration",
Run: func(cmd *cobra.Command, args []string) {
if err := azure.AzFuncCommand(AzTenantID, AzSubscription, AzOutputDirectory, cmd.Root().Version, AzVerbosity, AzWrapTable, AzMergedTable); err != nil {
azureLogger.FatalM(err.Error(), "func")
}
},
}
AzAutomationCommand = &cobra.Command{
Use: "automation",
Aliases: []string{"automations"},
Short: "Enumerates Azure Automation Accounts and their assets",
Run: func(cmd *cobra.Command, args []string) {
if err := azure.AzAutomationCommand(AzTenantID, AzSubscription, AzOutputDirectory, cmd.Root().Version, AzVerbosity, AzWrapTable, AzMergedTable); err != nil {
azureLogger.FatalM(err.Error(), "automation")
}
},
}
@@ -117,7 +172,7 @@ Enumerate storage accounts for a specific subscription:
func init() {
AzWhoamiCommand.Flags().BoolVarP(&AzWhoamiListRGsAlso, "list-rgs", "l", false, "Drill down to the resource group level")
AzTokenCacheCommand.Flags().BoolVarP(&AzTokenCacheListRGs, "list-rgs", "l", false, "Drill down to the resource group level")
// Global flags
AzCommands.PersistentFlags().StringVarP(&AzOutputFormat, "output", "o", "all", "[\"table\" | \"csv\" | \"all\" ]")
@@ -131,9 +186,13 @@ func init() {
AzCommands.AddCommand(
AzWhoamiCommand,
AzTokenCacheCommand,
AzRBACCommand,
AzVMsCommand,
AzStorageCommand,
AzACRCommand,
AzFuncCommand,
AzAutomationCommand,
AzInventoryCommand)
}
+4
View File
@@ -17,10 +17,14 @@ var AAD_USERS_TEST_FILE string
// Module names
const AZ_WHOAMI_MODULE_NAME = "whoami"
const AZ_TOKENCACHE_MODULE_NAME = "tokencache"
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_ACR_MODULE_NAME = "acr"
const AZ_FUNC_MODULE_NAME = "func"
const AZ_AUTOMATION_MODULE_NAME = "automation"
// Microsoft endpoints
const AZ_RESOURCE_MANAGER_ENDPOINT = "https://management.azure.com/"
+1 -1
View File
@@ -4,4 +4,4 @@ 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 = "2.0.5"
const CLOUDFOX_VERSION = "2.0.6"
+1
View File
@@ -116,6 +116,7 @@ require (
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
+2
View File
@@ -331,6 +331,8 @@ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9L
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
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/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+77 -3
View File
@@ -7,10 +7,12 @@ import (
"encoding/json"
"fmt"
"io"
"net"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
"sync/atomic"
"time"
@@ -34,6 +36,23 @@ var (
UtilsFs = afero.NewOsFs()
credsMap = map[string]aws.Credentials{}
ConfigMap = map[string]aws.Config{}
// MaxRetries controls the maximum number of retry attempts for AWS API calls.
// The default AWS SDK behavior is 3 attempts. Set to 0 for no retries, which
// is useful when running with limited-permission credentials to avoid slow
// timeouts on access-denied errors.
MaxRetries = 3
// unreachableRegions tracks regions that failed a TCP connectivity check so
// they can be skipped for the remainder of the run. Protected by a mutex
// since multiple goroutines may probe regions concurrently.
unreachableRegions = make(map[string]bool)
unreachableRegionsMu sync.RWMutex
// RegionReachabilityTimeout controls how long the TCP probe waits before
// declaring a region unreachable. Default 3 seconds is enough for any
// healthy AWS endpoint.
RegionReachabilityTimeout = 3 * time.Second
)
type CloudFoxRunData struct {
@@ -132,7 +151,7 @@ func AWSConfigFileLoader(AWSProfile string, version string, AwsMfaToken string)
if AwsMfaToken != "" {
cfg, err = config.LoadDefaultConfig(context.TODO(), config.WithSharedConfigProfile(AWSProfile), config.WithDefaultRegion("us-east-1"), config.WithRetryer(
func() aws.Retryer {
return retry.AddWithMaxAttempts(retry.NewStandard(), 3)
return retry.AddWithMaxAttempts(retry.NewStandard(), MaxRetries)
}), config.WithAssumeRoleCredentialOptions(func(options *stscreds.AssumeRoleOptions) {
options.TokenProvider = func() (string, error) {
return AwsMfaToken, nil
@@ -142,7 +161,7 @@ func AWSConfigFileLoader(AWSProfile string, version string, AwsMfaToken string)
} else {
cfg, err = config.LoadDefaultConfig(context.TODO(), config.WithSharedConfigProfile(AWSProfile), config.WithDefaultRegion("us-east-1"), config.WithRetryer(
func() aws.Retryer {
return retry.AddWithMaxAttempts(retry.NewStandard(), 3)
return retry.AddWithMaxAttempts(retry.NewStandard(), MaxRetries)
}), config.WithAssumeRoleCredentialOptions(func(options *stscreds.AssumeRoleOptions) {
options.TokenProvider = stscreds.StdinTokenProvider
}),
@@ -210,6 +229,58 @@ func AWSWhoami(awsProfile string, version string, AwsMfaToken string) (*sts.GetC
return CallerIdentity, err
}
// isRegionReachable does a fast TCP dial to the STS endpoint in the given region.
// STS is available in every region and is a lightweight endpoint to probe. If the
// connection succeeds, the region is reachable. Results are cached so each region
// is only probed once per run.
func isRegionReachable(region string) bool {
unreachableRegionsMu.RLock()
alreadyUnreachable := unreachableRegions[region]
unreachableRegionsMu.RUnlock()
if alreadyUnreachable {
return false
}
endpoint := fmt.Sprintf("sts.%s.amazonaws.com:443", region)
conn, err := net.DialTimeout("tcp", endpoint, RegionReachabilityTimeout)
if err != nil {
unreachableRegionsMu.Lock()
unreachableRegions[region] = true
unreachableRegionsMu.Unlock()
TxtLog.Warnf("Region %s is unreachable (probe to %s failed: %v) - skipping for this run", region, endpoint, err)
return false
}
conn.Close()
return true
}
// FilterReachableRegions takes a list of regions and returns only the ones that
// pass a TCP connectivity probe. Unreachable regions are probed in parallel and
// cached so subsequent calls return immediately.
func FilterReachableRegions(regions []string) []string {
// Probe all regions concurrently for speed
type probeResult struct {
region string
reachable bool
}
resultsChan := make(chan probeResult, len(regions))
for _, region := range regions {
go func(r string) {
resultsChan <- probeResult{region: r, reachable: isRegionReachable(r)}
}(region)
}
var reachableRegions []string
for range regions {
result := <-resultsChan
if result.reachable {
reachableRegions = append(reachableRegions, result.region)
}
}
return reachableRegions
}
func GetEnabledRegions(awsProfile string, version string, AwsMfaToken string) []string {
cacheKey := fmt.Sprintf("GetEnabledRegions-%s", awsProfile)
cached, found := Cache.Get(cacheKey)
@@ -234,12 +305,15 @@ func GetEnabledRegions(awsProfile string, version string, AwsMfaToken string) []
if err != nil {
TxtLog.Println(err)
}
return AWSRegions
reachableRegions := FilterReachableRegions(AWSRegions)
Cache.Set(cacheKey, reachableRegions, cache.DefaultExpiration)
return reachableRegions
}
for _, region := range regions.Regions {
enabledRegions = append(enabledRegions, *region.RegionName)
}
enabledRegions = FilterReachableRegions(enabledRegions)
Cache.Set(cacheKey, enabledRegions, cache.DefaultExpiration)
return enabledRegions
+45
View File
@@ -5,15 +5,18 @@ import (
"log"
"github.com/Azure/azure-sdk-for-go/profiles/latest/authorization/mgmt/authorization"
"github.com/Azure/azure-sdk-for-go/profiles/latest/automation/mgmt/automation"
"github.com/Azure/azure-sdk-for-go/profiles/latest/compute/mgmt/compute"
"github.com/Azure/azure-sdk-for-go/profiles/latest/network/mgmt/network"
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/resources"
"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/azure-sdk-for-go/profiles/latest/web/mgmt/web"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
"github.com/Azure/azure-sdk-for-go/services/graphrbac/1.6/graphrbac"
containerregistry "github.com/Azure/azure-sdk-for-go/services/preview/containerregistry/mgmt/2022-02-01-preview/containerregistry"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/azure/auth"
"github.com/BishopFox/cloudfox/globals"
@@ -147,6 +150,48 @@ func GetStorageClient(subscriptionID string) storage.AccountsClient {
return client
}
func GetContainerRegistriesClient(subscriptionID string) containerregistry.RegistriesClient {
client := containerregistry.NewRegistriesClient(subscriptionID)
a, err := getAuthorizer(globals.AZ_RESOURCE_MANAGER_ENDPOINT)
if err != nil {
log.Fatalf("failed to get container registries client: %s", err)
}
client.Authorizer = a
client.AddToUserAgent(globals.CLOUDFOX_USER_AGENT)
return client
}
func GetWebAppsClient(subscriptionID string) web.AppsClient {
client := web.NewAppsClient(subscriptionID)
a, err := getAuthorizer(globals.AZ_RESOURCE_MANAGER_ENDPOINT)
if err != nil {
log.Fatalf("failed to get web apps client: %s", err)
}
client.Authorizer = a
client.AddToUserAgent(globals.CLOUDFOX_USER_AGENT)
return client
}
func GetAutomationClients(subscriptionID string) (automation.AccountClient, automation.RunbookClient, automation.CredentialClient, automation.WebhookClient) {
authorizer, err := getAuthorizer(globals.AZ_RESOURCE_MANAGER_ENDPOINT)
if err != nil {
log.Fatalf("failed to get automation clients: %s", err)
}
accounts := automation.NewAccountClient(subscriptionID)
runbooks := automation.NewRunbookClient(subscriptionID)
credentials := automation.NewCredentialClient(subscriptionID)
webhooks := automation.NewWebhookClient(subscriptionID)
accounts.Authorizer = authorizer
runbooks.Authorizer = authorizer
credentials.Authorizer = authorizer
webhooks.Authorizer = authorizer
accounts.AddToUserAgent(globals.CLOUDFOX_USER_AGENT)
runbooks.AddToUserAgent(globals.CLOUDFOX_USER_AGENT)
credentials.AddToUserAgent(globals.CLOUDFOX_USER_AGENT)
webhooks.AddToUserAgent(globals.CLOUDFOX_USER_AGENT)
return accounts, runbooks, credentials, webhooks
}
func GetStorageAccountBlobClient(tenantID, storageAccountName string) (*azblob.Client, error) {
serviceURL := fmt.Sprintf("https://%s.blob.core.windows.net/", storageAccountName)
cred, err := azidentity.NewAzureCLICredential(&azidentity.AzureCLICredentialOptions{TenantID: tenantID})