From 4b5718a65cf4463cb077fd1153d25d60f6515533 Mon Sep 17 00:00:00 2001 From: Seppe Date: Thu, 25 Apr 2024 15:03:55 +0200 Subject: [PATCH] feature: add azure analysis service scanner (#221) * feature: add azure analytics service scanner Co-authored-by: Carlos Mendible <266546+cmendible@users.noreply.github.com> --- README.md | 1 + cmd/azqr/as.go | 28 ++++++ docs/content/en/docs/Overview/_index.md | 1 + go.mod | 1 + go.sum | 2 + internal/scan.go | 2 + internal/scanners/as/as.go | 64 ++++++++++++++ internal/scanners/as/rules.go | 79 +++++++++++++++++ internal/scanners/as/rules_test.go | 109 ++++++++++++++++++++++++ 9 files changed, 287 insertions(+) create mode 100644 cmd/azqr/as.go create mode 100644 internal/scanners/as/as.go create mode 100644 internal/scanners/as/rules.go create mode 100644 internal/scanners/as/rules_test.go diff --git a/README.md b/README.md index 6a4d6a48..6915810a 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ To learn more about the recommendations used by **Azure Quick Review (azqr)**, y **Azure Quick Review (azqr)** currently supports the following Azure services: +* Azure Analysis Service * Azure API Management * Azure App Configuration * Azure App Services diff --git a/cmd/azqr/as.go b/cmd/azqr/as.go new file mode 100644 index 00000000..6fc21c24 --- /dev/null +++ b/cmd/azqr/as.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/as" + "github.com/spf13/cobra" +) + +func init() { + scanCmd.AddCommand(asCmd) +} + +var asCmd = &cobra.Command{ + Use: "as", + Short: "Scan Azure Analysis Service", + Long: "Scan Azure Analysis Service", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + serviceScanners := []scanners.IAzureScanner{ + &as.AnalysisServicesScanner{}, + } + + scan(cmd, serviceScanners) + }, +} diff --git a/docs/content/en/docs/Overview/_index.md b/docs/content/en/docs/Overview/_index.md index 5c5e326e..bc456e44 100644 --- a/docs/content/en/docs/Overview/_index.md +++ b/docs/content/en/docs/Overview/_index.md @@ -58,6 +58,7 @@ To learn more about the recommendations used by **Azure Quick Review (azqr)**, y **Azure Quick Review (azqr)** currently supports the following Azure services: +* Azure Analysis Service * Azure API Management * Azure App Configuration * Azure App Services diff --git a/go.mod b/go.mod index 4716a792..b573c9a5 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.2 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/advisor/armadvisor v1.2.0 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/analysisservices/armanalysisservices v1.2.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/apimanagement/armapimanagement v1.1.1 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appconfiguration/armappconfiguration v1.1.1 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appcontainers/armappcontainers/v2 v2.1.0 diff --git a/go.sum b/go.sum index 7f997b5d..a8b5aa86 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/Azure/azure-sdk-for-go/sdk/internal v1.6.0 h1:sUFnFjzDUie80h24I7mrKtw github.com/Azure/azure-sdk-for-go/sdk/internal v1.6.0/go.mod h1:52JbnQTp15qg5mRkMBHwp0j0ZFwHJ42Sx3zVV5RE9p0= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/advisor/armadvisor v1.2.0 h1:3ddjPq/3A/oB2u7LdohEr900EGP5l1MnAiNc3EbY1E4= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/advisor/armadvisor v1.2.0/go.mod h1:oZ73p8dR7aZI+TJo5Ul92oCoVubMYPBo39eTsWa0AiQ= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/analysisservices/armanalysisservices v1.2.0 h1:iCQzouPqEJkjvgXooBv/AlUp16HHB+fR5S7CU1wDns8= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/analysisservices/armanalysisservices v1.2.0/go.mod h1:IDarmBQ4VwelvI5xEwBtDommOZYwb5xAsGS/ok1MYcg= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/apimanagement/armapimanagement v1.1.1 h1:jCkNVNpsEevyic4bmjgVjzVA4tMGSJpXNGirf+S+mDI= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/apimanagement/armapimanagement v1.1.1/go.mod h1:a0Ug1l73Il7EhrCJEEt2dGjlNjvphppZq5KqJdgnwuw= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appconfiguration/armappconfiguration v1.1.1 h1:iRc20pGuVlc1HwRO2bg0m1tfP9rkPB0K88trl8Fei2w= diff --git a/internal/scan.go b/internal/scan.go index c9249b1c..a6c84dda 100644 --- a/internal/scan.go +++ b/internal/scan.go @@ -34,6 +34,7 @@ import ( "github.com/Azure/azqr/internal/scanners/apim" "github.com/Azure/azqr/internal/scanners/appcs" "github.com/Azure/azqr/internal/scanners/appi" + "github.com/Azure/azqr/internal/scanners/as" "github.com/Azure/azqr/internal/scanners/asp" "github.com/Azure/azqr/internal/scanners/ca" "github.com/Azure/azqr/internal/scanners/cae" @@ -451,6 +452,7 @@ func GetScanners() []scanners.IAzureScanner { &apim.APIManagementScanner{}, &appcs.AppConfigurationScanner{}, &appi.AppInsightsScanner{}, + &as.AnalysisServicesScanner{}, &cae.ContainerAppsEnvironmentScanner{}, &ca.ContainerAppsScanner{}, &ci.ContainerInstanceScanner{}, diff --git a/internal/scanners/as/as.go b/internal/scanners/as/as.go new file mode 100644 index 00000000..fac5d42c --- /dev/null +++ b/internal/scanners/as/as.go @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package as + +import ( + "github.com/Azure/azqr/internal/scanners" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/analysisservices/armanalysisservices" +) + +// AnalysisServicesScanner - Scanner for Analysis Services +type AnalysisServicesScanner struct { + config *scanners.ScannerConfig + client *armanalysisservices.ServersClient +} + +// Init - Initializes the AnalysisServicesScanner +func (c *AnalysisServicesScanner) Init(config *scanners.ScannerConfig) error { + c.config = config + var err error + c.client, err = armanalysisservices.NewServersClient(config.SubscriptionID, config.Cred, config.ClientOptions) + return err +} + +// Scan - Scans all Analysis Services in a Resource Group +func (c *AnalysisServicesScanner) Scan(resourceGroupName string, scanContext *scanners.ScanContext) ([]scanners.AzureServiceResult, error) { + scanners.LogResourceGroupScan(c.config.SubscriptionID, resourceGroupName, "Analysis Services") + + workspaces, err := c.listWorkspaces(resourceGroupName) + if err != nil { + return nil, err + } + engine := scanners.RuleEngine{} + rules := c.GetRules() + results := []scanners.AzureServiceResult{} + + for _, ws := range workspaces { + rr := engine.EvaluateRules(rules, ws, scanContext) + + results = append(results, scanners.AzureServiceResult{ + SubscriptionID: c.config.SubscriptionID, + ResourceGroup: resourceGroupName, + ServiceName: *ws.Name, + Type: *ws.Type, + Location: *ws.Location, + Rules: rr, + }) + } + return results, nil +} + +func (c *AnalysisServicesScanner) listWorkspaces(resourceGroupName string) ([]*armanalysisservices.Server, error) { + pager := c.client.NewListByResourceGroupPager(resourceGroupName, nil) + + registries := make([]*armanalysisservices.Server, 0) + for pager.More() { + resp, err := pager.NextPage(c.config.Ctx) + if err != nil { + return nil, err + } + registries = append(registries, resp.Value...) + } + return registries, nil +} diff --git a/internal/scanners/as/rules.go b/internal/scanners/as/rules.go new file mode 100644 index 00000000..1bf5f93d --- /dev/null +++ b/internal/scanners/as/rules.go @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package as + +import ( + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/analysisservices/armanalysisservices" + "strings" + + "github.com/Azure/azqr/internal/scanners" +) + +// GetRules - Returns the rules for the AnalysisServicesScanner +func (a *AnalysisServicesScanner) GetRules() map[string]scanners.AzureRule { + return map[string]scanners.AzureRule{ + "as-001": { + Id: "as-001", + Category: scanners.RulesCategoryMonitoringAndAlerting, + Recommendation: "Azure Analysis Service should have diagnostic settings enabled", + Impact: scanners.ImpactLow, + Eval: func(target interface{}, scanContext *scanners.ScanContext) (bool, string) { + service := target.(*armanalysisservices.Server) + _, ok := scanContext.DiagnosticsSettings[strings.ToLower(*service.ID)] + return !ok, "" + }, + Url: "https://learn.microsoft.com/en-us/azure/analysis-services/analysis-services-logging", + }, + "as-002": { + Id: "as-002", + Category: scanners.RulesCategoryHighAvailability, + Recommendation: "Azure Analysis Service should have a SLA", + Impact: scanners.ImpactHigh, + Eval: func(target interface{}, scanContext *scanners.ScanContext) (bool, string) { + i := target.(*armanalysisservices.Server) + sku := *i.SKU.Tier + sla := "None" + if sku != armanalysisservices.SKUTierBasic { + sla = "99.9%" + } + return sla == "None", sla + }, + Url: "https://www.microsoft.com/licensing/docs/view/Service-Level-Agreements-SLA-for-Online-Services", + }, + "as-003": { + Id: "as-003", + Category: scanners.RulesCategoryHighAvailability, + Recommendation: "Azure Analysis Service SKU", + Impact: scanners.ImpactHigh, + Eval: func(target interface{}, scanContext *scanners.ScanContext) (bool, string) { + i := target.(*armanalysisservices.Server) + return false, string(*i.SKU.Name) + }, + Url: "https://azure.microsoft.com/en-us/pricing/details/analysis-services/", + }, + "as-004": { + Id: "as-004", + Category: scanners.RulesCategoryGovernance, + Recommendation: "Azure Analysis Service Name should comply with naming conventions", + Impact: scanners.ImpactLow, + Eval: func(target interface{}, scanContext *scanners.ScanContext) (bool, string) { + c := target.(*armanalysisservices.Server) + caf := strings.HasPrefix(*c.Name, "as") + return !caf, "" + }, + Url: "https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/resource-abbreviations", + }, + "as-005": { + Id: "as-005", + Category: scanners.RulesCategoryGovernance, + Recommendation: "Azure Analysis Service should have tags", + Impact: scanners.ImpactLow, + Eval: func(target interface{}, scanContext *scanners.ScanContext) (bool, string) { + c := target.(*armanalysisservices.Server) + return len(c.Tags) == 0, "" + }, + Url: "https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/tag-resources?tabs=json", + }, + } +} diff --git a/internal/scanners/as/rules_test.go b/internal/scanners/as/rules_test.go new file mode 100644 index 00000000..215f2b6c --- /dev/null +++ b/internal/scanners/as/rules_test.go @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package as + +import ( + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/analysisservices/armanalysisservices" + "reflect" + "testing" + + "github.com/Azure/azqr/internal/scanners" + "github.com/Azure/azqr/internal/to" +) + +func TestAnalysisServicesScanner_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: "AnalysisServicesScanner DiagnosticSettings", + fields: fields{ + rule: "as-001", + target: &armanalysisservices.Server{ + ID: to.Ptr("test"), + }, + scanContext: &scanners.ScanContext{ + DiagnosticsSettings: map[string]bool{ + "test": true, + }, + }, + }, + want: want{ + broken: false, + result: "", + }, + }, + { + name: "AnalysisServicesScanner SLA", + fields: fields{ + rule: "as-002", + target: &armanalysisservices.Server{ + SKU: &armanalysisservices.ResourceSKU{ + Tier: to.Ptr(armanalysisservices.SKUTierBasic), + }, + }, + scanContext: &scanners.ScanContext{}, + }, + want: want{ + broken: true, + result: "None", + }, + }, + { + name: "AnalysisServicesScanner SLA", + fields: fields{ + rule: "as-002", + target: &armanalysisservices.Server{ + SKU: &armanalysisservices.ResourceSKU{ + Tier: to.Ptr(armanalysisservices.SKUTierStandard), + }, + }, + scanContext: &scanners.ScanContext{}, + }, + want: want{ + broken: false, + result: "99.9%", + }, + }, + { + name: "AnalysisServicesScanner CAF", + fields: fields{ + rule: "as-004", + target: &armanalysisservices.Server{ + Name: to.Ptr("as-test"), + }, + scanContext: &scanners.ScanContext{}, + }, + want: want{ + broken: false, + result: "", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &AnalysisServicesScanner{} + 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("AnalysisServicesScanner Rule.Eval() = %v, want %v", got, tt.want) + } + }) + } +}