mirror of
https://github.com/BishopFox/cloudfox.git
synced 2026-11-07 09:56:01 -10:00
Added Azure automation command
This commit is contained in:
5 files changed
+234
No files matched your search
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
@@ -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/"
|
||||
|
||||
@@ -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})
|
||||
|
||||
Reference in new issue
Block a user