diff --git a/.gitignore b/.gitignore index 922d11fd..ee3f87dd 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,8 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out +# Jetbrains IDE +.idea/ # Dependency directories (remove the comment below to include it) # vendor/ diff --git a/README.md b/README.md index 8ada7b7e..72ad4e20 100644 --- a/README.md +++ b/README.md @@ -86,12 +86,14 @@ To learn more about the recommendations used by **Azure Quick Review (azqr)**, y * Azure Database for PostgreSQL Single Server * Azure Event Grid * Azure Event Hub +* Azure ExpressRoute Gateway * Azure Firewall * Azure Front Door * Azure Functions * Azure Key Vault * Azure Kubernetes Service * Azure Load Balancer +* Azure Local Gateway * Azure Logic Apps * Azure Managed Grafana * Azure Service Bus @@ -107,6 +109,7 @@ To learn more about the recommendations used by **Azure Quick Review (azqr)**, y * Azure Virtual Machine * Azure Virtual Network * Azure Virtual WAN +* Azure VPN Gateway * Azure Web PubSub ## Usage diff --git a/cmd/azqr/vgw.go b/cmd/azqr/vgw.go new file mode 100644 index 00000000..33050af3 --- /dev/null +++ b/cmd/azqr/vgw.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/vgw" + "github.com/spf13/cobra" +) + +func init() { + scanCmd.AddCommand(vgwCmd) +} + +var vgwCmd = &cobra.Command{ + Use: "vgw", + Short: "Scan Virtual Network Gateway", + Long: "Scan Virtual Network Gateway", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + serviceScanners := []scanners.IAzureScanner{ + &vgw.VirtualNetworkGatewayScanner{}, + } + + scan(cmd, serviceScanners) + }, +} diff --git a/docs/content/en/docs/Overview/_index.md b/docs/content/en/docs/Overview/_index.md index 7095ac2b..c815638f 100644 --- a/docs/content/en/docs/Overview/_index.md +++ b/docs/content/en/docs/Overview/_index.md @@ -81,12 +81,14 @@ To learn more about the recommendations used by **Azure Quick Review (azqr)**, y * Azure Database for PostgreSQL Single Server * Azure Event Grid * Azure Event Hub +* Azure ExpressRoute Gateway * Azure Firewall * Azure Front Door * Azure Functions * Azure Key Vault * Azure Kubernetes Service * Azure Load Balancer +* Azure Local Gateway * Azure Logic Apps * Azure Managed Grafana * Azure Service Bus @@ -103,6 +105,7 @@ To learn more about the recommendations used by **Azure Quick Review (azqr)**, y * Azure Virtual Machine Scale Set * Azure Virtual Network * Azure Virtual WAN +* Azure VPN Gateway * Azure Web PubSub ## Code of Conduct diff --git a/internal/scan.go b/internal/scan.go index e455ec8d..58bcf5a4 100644 --- a/internal/scan.go +++ b/internal/scan.go @@ -60,6 +60,7 @@ import ( "github.com/Azure/azqr/internal/scanners/st" "github.com/Azure/azqr/internal/scanners/synw" "github.com/Azure/azqr/internal/scanners/traf" + "github.com/Azure/azqr/internal/scanners/vgw" "github.com/Azure/azqr/internal/scanners/vm" "github.com/Azure/azqr/internal/scanners/vmss" "github.com/Azure/azqr/internal/scanners/vnet" @@ -483,6 +484,7 @@ func GetScanners() []scanners.IAzureScanner { &vm.VirtualMachineScanner{}, &vmss.VirtualMachineScaleSetScanner{}, &vnet.VirtualNetworkScanner{}, + &vgw.VirtualNetworkGatewayScanner{}, &wps.WebPubSubScanner{}, } } diff --git a/internal/scanners/vgw/rules.go b/internal/scanners/vgw/rules.go new file mode 100644 index 00000000..35b7778c --- /dev/null +++ b/internal/scanners/vgw/rules.go @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package vgw + +import ( + "strings" + + "github.com/Azure/azqr/internal/scanners" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v5" +) + +// GetRules - Returns the rules for the VirtualNetworkGatewayScanner +func (a *VirtualNetworkGatewayScanner) GetRules() map[string]scanners.AzureRule { + return a.GetVirtualNetworkGatewayRules() +} + +// GetVirtualNetworkGatewayRules - Returns the rules for the VirtualNetworkGatewayScanner +func (a *VirtualNetworkGatewayScanner) GetVirtualNetworkGatewayRules() map[string]scanners.AzureRule { + return map[string]scanners.AzureRule{ + "vgw-001": { + Id: "vgw-001", + Category: scanners.RulesCategoryMonitoringAndAlerting, + Recommendation: "Virtual Network Gateway should have diagnostic settings enabled", + Impact: scanners.ImpactLow, + Eval: func(target interface{}, scanContext *scanners.ScanContext) (bool, string) { + service := target.(*armnetwork.VirtualNetworkGateway) + _, ok := scanContext.DiagnosticsSettings[strings.ToLower(*service.ID)] + return !ok, "" + }, + Url: "https://learn.microsoft.com/en-us/azure/vpn-gateway/monitor-vpn-gateway", + }, + "vgw-002": { + Id: "vgw-002", + Category: scanners.RulesCategoryGovernance, + Recommendation: "Virtual Network Gateway Name should comply with naming conventions", + Impact: scanners.ImpactLow, + Eval: func(target interface{}, scanContext *scanners.ScanContext) (bool, string) { + c := target.(*armnetwork.VirtualNetworkGateway) + switch *c.Properties.GatewayType { + case armnetwork.VirtualNetworkGatewayTypeVPN: + return !strings.HasPrefix(*c.Name, "vpng"), "" + case armnetwork.VirtualNetworkGatewayTypeExpressRoute: + return !strings.HasPrefix(*c.Name, "ergw"), "" + default: + return !strings.HasPrefix(*c.Name, "lgw"), "" + } + }, + Url: "https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/resource-abbreviations", + }, + "vgw-003": { + Id: "vgw-003", + Category: scanners.RulesCategoryGovernance, + Recommendation: "Virtual Network Gateway should have tags", + Impact: scanners.ImpactLow, + Eval: func(target interface{}, scanContext *scanners.ScanContext) (bool, string) { + c := target.(*armnetwork.VirtualNetworkGateway) + return len(c.Tags) == 0, "" + }, + Url: "https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/tag-resources?tabs=json", + }, + "vgw-004": { + Id: "vgw-004", + Category: scanners.RulesCategoryHighAvailability, + Recommendation: "Virtual Network Gateway should have a SLA", + Impact: scanners.ImpactHigh, + Eval: func(target interface{}, scanContext *scanners.ScanContext) (bool, string) { + g := target.(*armnetwork.VirtualNetworkGateway) + sku := string(*g.Properties.SKU.Tier) + sla := "99.9%" + if sku != string(armnetwork.VirtualNetworkGatewaySKUTierBasic) { + sla = "99.95%" + } + return false, sla + }, + Url: "https://www.microsoft.com/licensing/docs/view/Service-Level-Agreements-SLA-for-Online-Services", + }, + "vgw-005": { + Id: "vgw-005", + Category: scanners.RulesCategoryHighAvailability, + Recommendation: "Storage should have availability zones enabled", + Impact: scanners.ImpactHigh, + Eval: func(target interface{}, scanContext *scanners.ScanContext) (bool, string) { + g := target.(*armnetwork.VirtualNetworkGateway) + sku := string(*g.Properties.SKU.Name) + return !strings.HasSuffix(strings.ToLower(sku), "az"), "" + }, + Url: "https://learn.microsoft.com/en-us/azure/vpn-gateway/create-zone-redundant-vnet-gateway", + }, + } +} diff --git a/internal/scanners/vgw/rules_test.go b/internal/scanners/vgw/rules_test.go new file mode 100644 index 00000000..50e7dbd3 --- /dev/null +++ b/internal/scanners/vgw/rules_test.go @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package vgw + +import ( + "reflect" + "testing" + + "github.com/Azure/azqr/internal/scanners" + "github.com/Azure/azqr/internal/to" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v5" +) + +func TestVirtualNetworkGatewayScanner_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: "VirtualNetworkGatewayScanner DiagnosticSettings", + fields: fields{ + rule: "vgw-001", + target: &armnetwork.VirtualNetworkGateway{ + ID: to.Ptr("test"), + }, + scanContext: &scanners.ScanContext{ + DiagnosticsSettings: map[string]bool{ + "test": true, + }, + }, + }, + want: want{ + broken: false, + result: "", + }, + }, + { + name: "VirtualNetworkGatewayScanner CAF", + fields: fields{ + rule: "vgw-002", + target: &armnetwork.VirtualNetworkGateway{ + Name: to.Ptr("vpng-test"), + Properties: &armnetwork.VirtualNetworkGatewayPropertiesFormat{ + GatewayType: to.Ptr(armnetwork.VirtualNetworkGatewayTypeVPN), + }, + }, + scanContext: &scanners.ScanContext{}, + }, + want: want{ + broken: false, + result: "", + }, + }, + { + name: "VirtualNetworkGatewayScanner SLA 99.9%", + fields: fields{ + rule: "vgw-004", + target: &armnetwork.VirtualNetworkGateway{ + Properties: &armnetwork.VirtualNetworkGatewayPropertiesFormat{ + SKU: &armnetwork.VirtualNetworkGatewaySKU{ + Tier: to.Ptr(armnetwork.VirtualNetworkGatewaySKUTierBasic), + }}, + }, + scanContext: &scanners.ScanContext{}, + }, + want: want{ + broken: false, + result: "99.9%", + }, + }, + { + name: "VirtualNetworkGatewayScanner SLA 99.9%", + fields: fields{ + rule: "vgw-004", + target: &armnetwork.VirtualNetworkGateway{ + Properties: &armnetwork.VirtualNetworkGatewayPropertiesFormat{ + SKU: &armnetwork.VirtualNetworkGatewaySKU{ + Tier: to.Ptr(armnetwork.VirtualNetworkGatewaySKUTierErGw1AZ), + }}, + }, + scanContext: &scanners.ScanContext{}, + }, + want: want{ + broken: false, + result: "99.95%", + }, + }, + { + name: "VirtualNetworkGatewayScanner without AZ", + fields: fields{ + rule: "vgw-005", + target: &armnetwork.VirtualNetworkGateway{ + Properties: &armnetwork.VirtualNetworkGatewayPropertiesFormat{ + SKU: &armnetwork.VirtualNetworkGatewaySKU{ + Name: to.Ptr(armnetwork.VirtualNetworkGatewaySKUNameBasic), + }}, + }, + scanContext: &scanners.ScanContext{}, + }, + want: want{ + broken: true, + result: "", + }, + }, + { + name: "VirtualNetworkGatewayScanner with AZ", + fields: fields{ + rule: "vgw-005", + target: &armnetwork.VirtualNetworkGateway{ + Properties: &armnetwork.VirtualNetworkGatewayPropertiesFormat{ + SKU: &armnetwork.VirtualNetworkGatewaySKU{ + Name: to.Ptr(armnetwork.VirtualNetworkGatewaySKUNameErGw1AZ), + }}, + }, + scanContext: &scanners.ScanContext{}, + }, + want: want{ + broken: false, + result: "", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &VirtualNetworkGatewayScanner{} + rules := s.GetVirtualNetworkGatewayRules() + 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("VirtualNetworkGatewayScanner Rule.Eval() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/scanners/vgw/vgw.go b/internal/scanners/vgw/vgw.go new file mode 100644 index 00000000..6105b644 --- /dev/null +++ b/internal/scanners/vgw/vgw.go @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package vgw + +import ( + "github.com/Azure/azqr/internal/scanners" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v5" +) + +// VirtualNetworkGatewayScanner - Scanner for VPN Gateway +type VirtualNetworkGatewayScanner struct { + config *scanners.ScannerConfig + client *armnetwork.VirtualNetworkGatewaysClient +} + +// Init - Initializes the VPN Gateway +func (c *VirtualNetworkGatewayScanner) Init(config *scanners.ScannerConfig) error { + c.config = config + var err error + c.client, err = armnetwork.NewVirtualNetworkGatewaysClient(config.SubscriptionID, config.Cred, config.ClientOptions) + return err +} + +// Scan - Scans all VirtualNetwork in a Resource Group +func (c *VirtualNetworkGatewayScanner) Scan(resourceGroupName string, scanContext *scanners.ScanContext) ([]scanners.AzureServiceResult, error) { + scanners.LogResourceGroupScan(c.config.SubscriptionID, resourceGroupName, "VPN Gateway") + + vpns, err := c.listVirtualNetworkGateways(resourceGroupName) + if err != nil { + return nil, err + } + engine := scanners.RuleEngine{} + rules := c.GetVirtualNetworkGatewayRules() + results := []scanners.AzureServiceResult{} + + for _, w := range vpns { + rr := engine.EvaluateRules(rules, w, scanContext) + + results = append(results, scanners.AzureServiceResult{ + SubscriptionID: c.config.SubscriptionID, + ResourceGroup: resourceGroupName, + ServiceName: *w.Name, + Type: *w.Type, + Location: *w.Location, + Rules: rr, + }) + } + return results, nil +} + +func (c *VirtualNetworkGatewayScanner) listVirtualNetworkGateways(resourceGroupName string) ([]*armnetwork.VirtualNetworkGateway, error) { + pager := c.client.NewListPager(resourceGroupName, nil) + + vpns := make([]*armnetwork.VirtualNetworkGateway, 0) + for pager.More() { + resp, err := pager.NextPage(c.config.Ctx) + if err != nil { + return nil, err + } + vpns = append(vpns, resp.Value...) + } + return vpns, nil +}