Added Azure automation command

This commit is contained in:
Bastien Faure committed 2026-10-29 23:55:08 -04:00
1 parent e3a1803cce
commit 2f0cfd7b1f
5 files changed
+234

No files matched your search

+164
View File
@@ -0,0 +1,164 @@
package azure
import (
"context"
"encoding/json"
"fmt"
"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"
"github.com/fatih/color"
"github.com/kyokomi/emoji"
)
var (
getAutomationAccounts = getAutomationAccountsOriginal
getAutomationAssets = getAutomationAssetsOriginal
)
type automationAssets struct {
Runbooks []automation.Runbook `json:"runbooks"`
Credentials []automation.Credential `json:"credentials"`
Webhooks []automation.Webhook `json:"webhooks"`
}
func AzAutomationCommand(tenantID, subscription, outputDirectory, version string, verbosity int, wrap, merged bool) error {
if tenantID != "" && subscription == "" && merged {
tenant := populateTenant(tenantID)
if ptr.ToString(tenant.ID) == "" {
return fmt.Errorf("could not find Azure tenant %q", tenantID)
}
subs := GetSubscriptionsPerTenantID(ptr.ToString(tenant.ID))
var rows [][]string
var loot strings.Builder
for _, sub := range subs {
subRows, subLoot, warnings, err := getAutomationData(sub)
if err != nil {
return err
}
rows = append(rows, subRows...)
loot.WriteString(subLoot)
for _, warning := range warnings {
fmt.Printf("[%s] %s\n", color.CyanString(globals.AZ_AUTOMATION_MODULE_NAME), warning)
}
}
if len(subs) > 0 {
writeScopedAzureOutput(outputDirectory, subs[0], globals.AZ_AUTOMATION_MODULE_NAME, []string{"Automation Account", "Subscription Name", "Subscription ID", "Resource Group", "Runbooks", "Credentials", "Webhooks"}, rows, "automation-assets", loot.String(), verbosity, wrap, true)
}
return nil
}
return runAzureScopedModule(tenantID, subscription, merged, func(sub subscriptions.Subscription) error {
fmt.Printf("[%s][%s] Enumerating Automation Accounts for subscription %s (%s)\n", color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", version)), color.CyanString(globals.AZ_AUTOMATION_MODULE_NAME), ptr.ToString(sub.DisplayName), ptr.ToString(sub.SubscriptionID))
rows, loot, warnings, err := getAutomationData(sub)
if err != nil {
return err
}
for _, warning := range warnings {
fmt.Printf("[%s] %s\n", color.CyanString(globals.AZ_AUTOMATION_MODULE_NAME), warning)
}
writeScopedAzureOutput(outputDirectory, sub, globals.AZ_AUTOMATION_MODULE_NAME, []string{"Automation Account", "Subscription Name", "Subscription ID", "Resource Group", "Runbooks", "Credentials", "Webhooks"}, rows, "automation-assets", loot, verbosity, wrap, false)
return nil
})
}
func getAutomationData(subscription subscriptions.Subscription) ([][]string, string, []error, error) {
subscriptionID := ptr.ToString(subscription.SubscriptionID)
accounts, err := getAutomationAccounts(subscriptionID)
if err != nil {
return nil, "", nil, fmt.Errorf("could not enumerate Automation Accounts for subscription %s: %w", subscriptionID, err)
}
var rows [][]string
var loot strings.Builder
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 {
encoded, marshalErr := json.MarshalIndent(automationAssetsForLoot(assets), "", " ")
if marshalErr == nil {
fmt.Fprintf(&loot, "===============================================================\nAutomation Account: %s\nResource Group: %s\n%s\n\n", name, rg, encoded)
}
}
}
sort.Slice(rows, func(i, j int) bool { return rows[i][0] < rows[j][0] })
return rows, loot.String(), warnings, nil
}
func automationAssetsForLoot(assets automationAssets) map[string]any {
runbooks := make([]map[string]any, 0, len(assets.Runbooks))
for _, runbook := range assets.Runbooks {
runbooks = append(runbooks, map[string]any{"name": ptr.ToString(runbook.Name), "properties": runbook.RunbookProperties})
}
credentials := make([]map[string]any, 0, len(assets.Credentials))
for _, credential := range assets.Credentials {
credentials = append(credentials, map[string]any{"name": ptr.ToString(credential.Name), "properties": credential.CredentialProperties})
}
webhooks := make([]map[string]any, 0, len(assets.Webhooks))
for _, webhook := range assets.Webhooks {
webhooks = append(webhooks, map[string]any{"name": ptr.ToString(webhook.Name), "properties": webhook.WebhookProperties})
}
return map[string]any{"runbooks": runbooks, "credentials": credentials, "webhooks": webhooks}
}
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
}
}
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
}
+37
View File
@@ -0,0 +1,37 @@
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")}}, 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)
}
for _, value := range []string{"daily", "service-account", "deploy"} {
if !strings.Contains(loot, value) {
t.Errorf("loot missing %q: %s", value, loot)
}
}
}
+11
View File
@@ -141,6 +141,16 @@ Enumerate container registries for a specific subscription:
}
},
}
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 {
log.Fatal(err)
}
},
}
)
func init() {
@@ -164,6 +174,7 @@ func init() {
AzStorageCommand,
AzACRCommand,
AzFuncCommand,
AzAutomationCommand,
AzInventoryCommand)
}
+1
View File
@@ -23,6 +23,7 @@ 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/"
+21
View File
@@ -5,6 +5,7 @@ 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"
@@ -171,6 +172,26 @@ func GetWebAppsClient(subscriptionID string) web.AppsClient {
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})