diff --git a/README.md b/README.md index 6915810a..8ada7b7e 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,7 @@ To learn more about the recommendations used by **Azure Quick Review (azqr)**, y * Azure Kubernetes Service * Azure Load Balancer * Azure Logic Apps +* Azure Managed Grafana * Azure Service Bus * Azure SignalR Service * Azure SQL Server diff --git a/cmd/azqr/amg.go b/cmd/azqr/amg.go new file mode 100644 index 00000000..e46341c6 --- /dev/null +++ b/cmd/azqr/amg.go @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package azqr + +import ( + "github.com/Azure/azqr/internal/scanners" + "github.com/Azure/azqr/internal/scanners/amg" + "github.com/spf13/cobra" +) + +func init() { + scanCmd.AddCommand(amgCmd) +} + +var amgCmd = &cobra.Command{ + Use: "amg", + Short: "Scan Azure Managed Grafana", + Long: "Scan Azure Managed Grafana", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + serviceScanners := []scanners.IAzureScanner{ + &amg.ManagedGrafanaScanner{}, + } + + scan(cmd, serviceScanners) + }, +} diff --git a/docs/content/en/docs/Overview/_index.md b/docs/content/en/docs/Overview/_index.md index bc456e44..7095ac2b 100644 --- a/docs/content/en/docs/Overview/_index.md +++ b/docs/content/en/docs/Overview/_index.md @@ -88,6 +88,7 @@ To learn more about the recommendations used by **Azure Quick Review (azqr)**, y * Azure Kubernetes Service * Azure Load Balancer * Azure Logic Apps +* Azure Managed Grafana * Azure Service Bus * Azure SignalR Service * Azure SQL Server diff --git a/go.mod b/go.mod index b573c9a5..e1ccbf56 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v4 v4.8.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos v1.0.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/costmanagement/armcostmanagement v1.1.1 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dashboard/armdashboard v1.2.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/databricks/armdatabricks v1.1.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/datafactory/armdatafactory v1.3.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/eventgrid/armeventgrid v1.0.0 diff --git a/go.sum b/go.sum index a8b5aa86..0ff7290a 100644 --- a/go.sum +++ b/go.sum @@ -34,6 +34,8 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos v1.0.0 h1 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos v1.0.0/go.mod h1:Qpe/qN9d5IQ7WPtTXMRCd6+BWTnhi3sxXVys6oJ5Vho= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/costmanagement/armcostmanagement v1.1.1 h1:ehSLdbLah6kk6HTVc6e/lrbmbz7MMbpNxkOd3OYlhB0= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/costmanagement/armcostmanagement v1.1.1/go.mod h1:Am1cUioOk0HdZIsjpXJkQ4RIeQbwYsW6LkNIc5z/5XY= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dashboard/armdashboard v1.2.0 h1:MRPU8Bge2f9tkfG3PCr4vEnqXl8XOSjlhuK3l+8Hvkc= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dashboard/armdashboard v1.2.0/go.mod h1:xYrOYxajQvXMlp6M1E3amlaqPDXspyJxmjqTsGo6Jmw= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/databricks/armdatabricks v1.1.0 h1:rQyNHB/4ntzvm5F9WAiaAl7jWII+jaI4rL6sSWxTNeM= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/databricks/armdatabricks v1.1.0/go.mod h1:4jtknLqzaPtwIz8Y9NBp2rXxeA7BbSICWBD0FDzG2VM= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/datafactory/armdatafactory v1.3.0 h1:pmKRJksZidUYbOMQ2wtVm4L9q0BadVfBsF/fPKUUnjg= diff --git a/internal/scan.go b/internal/scan.go index a6c84dda..e455ec8d 100644 --- a/internal/scan.go +++ b/internal/scan.go @@ -31,6 +31,7 @@ import ( "github.com/Azure/azqr/internal/scanners/afw" "github.com/Azure/azqr/internal/scanners/agw" "github.com/Azure/azqr/internal/scanners/aks" + "github.com/Azure/azqr/internal/scanners/amg" "github.com/Azure/azqr/internal/scanners/apim" "github.com/Azure/azqr/internal/scanners/appcs" "github.com/Azure/azqr/internal/scanners/appi" @@ -449,6 +450,7 @@ func GetScanners() []scanners.IAzureScanner { &afw.FirewallScanner{}, &agw.ApplicationGatewayScanner{}, &aks.AKSScanner{}, + &amg.ManagedGrafanaScanner{}, &apim.APIManagementScanner{}, &appcs.AppConfigurationScanner{}, &appi.AppInsightsScanner{}, diff --git a/internal/scanners/amg/amg.go b/internal/scanners/amg/amg.go new file mode 100644 index 00000000..219e6a46 --- /dev/null +++ b/internal/scanners/amg/amg.go @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package amg + +import ( + "github.com/Azure/azqr/internal/scanners" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dashboard/armdashboard" +) + +// ManagedGrafanaScanner - Scanner for Managed Grafana +type ManagedGrafanaScanner struct { + config *scanners.ScannerConfig + grafanaClient *armdashboard.GrafanaClient +} + +// Init - Initializes the ManagedGrafanaScanner Scanner +func (a *ManagedGrafanaScanner) Init(config *scanners.ScannerConfig) error { + a.config = config + var err error + a.grafanaClient, _ = armdashboard.NewGrafanaClient(config.SubscriptionID, a.config.Cred, a.config.ClientOptions) + return err +} + +// Scan - Scans all Managed Grafana in a Resource Group +func (a *ManagedGrafanaScanner) Scan(resourceGroupName string, scanContext *scanners.ScanContext) ([]scanners.AzureServiceResult, error) { + scanners.LogResourceGroupScan(a.config.SubscriptionID, resourceGroupName, "Managed Grafana") + + workspaces, err := a.listWorkspaces(resourceGroupName) + if err != nil { + return nil, err + } + engine := scanners.RuleEngine{} + rules := a.GetRules() + results := []scanners.AzureServiceResult{} + + for _, g := range workspaces { + rr := engine.EvaluateRules(rules, g, scanContext) + + results = append(results, scanners.AzureServiceResult{ + SubscriptionID: a.config.SubscriptionID, + ResourceGroup: resourceGroupName, + Location: *g.Location, + Type: *g.Type, + ServiceName: *g.Name, + Rules: rr, + }) + } + return results, nil +} + +func (a *ManagedGrafanaScanner) listWorkspaces(resourceGroupName string) ([]*armdashboard.ManagedGrafana, error) { + pager := a.grafanaClient.NewListByResourceGroupPager(resourceGroupName, nil) + + workspaces := make([]*armdashboard.ManagedGrafana, 0) + for pager.More() { + resp, err := pager.NextPage(a.config.Ctx) + if err != nil { + return nil, err + } + workspaces = append(workspaces, resp.Value...) + } + + return workspaces, nil +} diff --git a/internal/scanners/amg/rules.go b/internal/scanners/amg/rules.go new file mode 100644 index 00000000..9e9a1b88 --- /dev/null +++ b/internal/scanners/amg/rules.go @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package amg + +import ( + "strings" + + "github.com/Azure/azqr/internal/scanners" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dashboard/armdashboard" +) + +// GetRules - Returns the rules for the ManagedGrafanaScanner +func (a *ManagedGrafanaScanner) GetRules() map[string]scanners.AzureRule { + return map[string]scanners.AzureRule{ + "amg-001": { + Id: "amg-001", + Category: scanners.RulesCategoryGovernance, + Recommendation: "Azure Managed Grafana name should comply with naming conventions", + Impact: scanners.ImpactLow, + Eval: func(target interface{}, scanContext *scanners.ScanContext) (bool, string) { + c := target.(*armdashboard.ManagedGrafana) + caf := strings.HasPrefix(*c.Name, "amg") + return !caf, "" + }, + Url: "https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/resource-abbreviations", + }, + "amg-002": { + Id: "amg-002", + Category: scanners.RulesCategoryHighAvailability, + Recommendation: "Azure Managed Grafana SLA", + Impact: scanners.ImpactHigh, + Eval: func(target interface{}, scanContext *scanners.ScanContext) (bool, string) { + c := target.(*armdashboard.ManagedGrafana) + sku := "" + if c.SKU != nil && c.SKU.Name != nil { + sku = strings.ToLower(*c.SKU.Name) + } + sla := "None" + if strings.Contains(sku, "standard") { + sla = "99.9%" + } + return sla == "None", sla + }, + Url: "https://www.microsoft.com/licensing/docs/view/Service-Level-Agreements-SLA-for-Online-Services", + }, + "amg-003": { + Id: "amg-003", + Category: scanners.RulesCategoryGovernance, + Recommendation: "Azure Managed Grafana should have tags", + Impact: scanners.ImpactLow, + Eval: func(target interface{}, scanContext *scanners.ScanContext) (bool, string) { + c := target.(*armdashboard.ManagedGrafana) + return len(c.Tags) == 0, "" + }, + Url: "https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/tag-resources?tabs=json", + }, + "amg-004": { + Id: "amg-004", + Category: scanners.RulesCategorySecurity, + Recommendation: "Azure Managed Grafana should disable public network access", + Impact: scanners.ImpactHigh, + Eval: func(target interface{}, scanContext *scanners.ScanContext) (bool, string) { + c := target.(*armdashboard.ManagedGrafana) + return *c.Properties.PublicNetworkAccess == armdashboard.PublicNetworkAccessEnabled, "" + }, + Url: "https://learn.microsoft.com/en-us/security/benchmark/azure/baselines/azure-synapse-analytics-security-baseline?toc=%2Fazure%2Fsynapse-analytics%2Ftoc.json", + }, + "amg-005": { + Id: "amg-005", + Category: scanners.RulesCategoryHighAvailability, + Recommendation: "Azure Managed Grafana should have availability zones enabled", + Impact: scanners.ImpactHigh, + Eval: func(target interface{}, scanContext *scanners.ScanContext) (bool, string) { + c := target.(*armdashboard.ManagedGrafana) + return *c.Properties.ZoneRedundancy == armdashboard.ZoneRedundancyDisabled, "" + }, + Url: "https://learn.microsoft.com/en-us/azure/managed-grafana/high-availability", + }, + } +} diff --git a/internal/scanners/amg/rules_test.go b/internal/scanners/amg/rules_test.go new file mode 100644 index 00000000..c98943ee --- /dev/null +++ b/internal/scanners/amg/rules_test.go @@ -0,0 +1,150 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package amg + +import ( + "reflect" + "testing" + + "github.com/Azure/azqr/internal/scanners" + "github.com/Azure/azqr/internal/to" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dashboard/armdashboard" +) + +func TestManagedGrafanaScanner_Rules(t *testing.T) { + type fields struct { + rule string + target interface{} + scanContext *scanners.ScanContext + } + type want struct { + broken bool + result string + } + tests := []struct { + name string + fields fields + want want + }{ + { + name: "ManagedGrafanaScanner availability zones enabled", + fields: fields{ + rule: "amg-005", + target: &armdashboard.ManagedGrafana{ + Properties: &armdashboard.ManagedGrafanaProperties{ + ZoneRedundancy: to.Ptr(armdashboard.ZoneRedundancyEnabled), + }, + }, + scanContext: &scanners.ScanContext{}, + }, + want: want{ + broken: false, + result: "", + }, + }, { + name: "ManagedGrafanaScanner availability zones enabled", + fields: fields{ + rule: "amg-005", + target: &armdashboard.ManagedGrafana{ + Properties: &armdashboard.ManagedGrafanaProperties{ + ZoneRedundancy: to.Ptr(armdashboard.ZoneRedundancyDisabled), + }, + }, + scanContext: &scanners.ScanContext{}, + }, + want: want{ + broken: true, + result: "", + }, + }, { + name: "ManagedGrafanaScanner SLA Standard", + fields: fields{ + rule: "amg-002", + target: &armdashboard.ManagedGrafana{ + SKU: &armdashboard.ResourceSKU{ + Name: to.Ptr("Standard"), + }, + }, + scanContext: &scanners.ScanContext{}, + }, + want: want{ + broken: false, + result: "99.9%", + }, + }, { + name: "ManagedGrafanaScanner SLA Basic", + fields: fields{ + rule: "amg-002", + target: &armdashboard.ManagedGrafana{ + SKU: &armdashboard.ResourceSKU{ + Name: to.Ptr("Basic"), + }, + }, + scanContext: &scanners.ScanContext{}, + }, + want: want{ + broken: true, + result: "None", + }, + }, + { + name: "ManagedGrafanaScanner CAF", + fields: fields{ + rule: "amg-001", + target: &armdashboard.ManagedGrafana{ + Name: to.Ptr("amg-test"), + }, + scanContext: &scanners.ScanContext{}, + }, + want: want{ + broken: false, + result: "", + }, + }, { + name: "ManagedGrafanaScanner Public network enabled", + fields: fields{ + rule: "amg-004", + target: &armdashboard.ManagedGrafana{ + Properties: &armdashboard.ManagedGrafanaProperties{ + PublicNetworkAccess: to.Ptr(armdashboard.PublicNetworkAccessEnabled), + }, + }, + scanContext: &scanners.ScanContext{}, + }, + want: want{ + broken: true, + result: "", + }, + }, { + name: "ManagedGrafanaScanner Public network disabled", + fields: fields{ + rule: "amg-004", + target: &armdashboard.ManagedGrafana{ + Properties: &armdashboard.ManagedGrafanaProperties{ + PublicNetworkAccess: to.Ptr(armdashboard.PublicNetworkAccessDisabled), + }, + }, + scanContext: &scanners.ScanContext{}, + }, + want: want{ + broken: false, + result: "", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &ManagedGrafanaScanner{} + rules := s.GetRules() + b, w := rules[tt.fields.rule].Eval(tt.fields.target, tt.fields.scanContext) + got := want{ + broken: b, + result: w, + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ManagedGrafanaScanner Rule.Eval() = %v, want %v", got, tt.want) + } + }) + } +}