Skip to content

Commit

Permalink
feature: Managed Grafana scanner (#233)
Browse files Browse the repository at this point in the history
feature: Managed Grafana scanner 

---------

Co-authored-by: Carlos Mendible <266546+cmendible@users.noreply.github.com>
  • Loading branch information
vanwinkelseppe and cmendible committed May 14, 2024
1 parent eddd627 commit a3c2156
Show file tree
Hide file tree
Showing 9 changed files with 331 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions cmd/azqr/amg.go
Original file line number Diff line number Diff line change
@@ -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)
},
}
1 change: 1 addition & 0 deletions docs/content/en/docs/Overview/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
2 changes: 2 additions & 0 deletions internal/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -449,6 +450,7 @@ func GetScanners() []scanners.IAzureScanner {
&afw.FirewallScanner{},
&agw.ApplicationGatewayScanner{},
&aks.AKSScanner{},
&amg.ManagedGrafanaScanner{},
&apim.APIManagementScanner{},
&appcs.AppConfigurationScanner{},
&appi.AppInsightsScanner{},
Expand Down
65 changes: 65 additions & 0 deletions internal/scanners/amg/amg.go
Original file line number Diff line number Diff line change
@@ -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
}
81 changes: 81 additions & 0 deletions internal/scanners/amg/rules.go
Original file line number Diff line number Diff line change
@@ -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",
},
}
}
150 changes: 150 additions & 0 deletions internal/scanners/amg/rules_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}

0 comments on commit a3c2156

Please sign in to comment.