Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: add Azure Virtual Network Gateway scanner #207

Closed
wants to merge 13 commits into from
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/

Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 Service Bus
* Azure SignalR Service
Expand All @@ -106,6 +108,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
Expand Down
28 changes: 28 additions & 0 deletions cmd/azqr/vgw.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/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)
},
}
4 changes: 3 additions & 1 deletion docs/content/en/docs/Overview/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 Service Bus
* Azure SignalR Service
Expand All @@ -99,9 +101,9 @@ To learn more about the recommendations used by **Azure Quick Review (azqr)**, y
* Azure Synapse Dedicated SQL Pool
* Azure Traffic Manager
* Azure Virtual Machine
* Azure Virtual Machine Scale Set
* Azure Virtual Network
* Azure Virtual WAN
* Azure VPN Gateway
vanwinkelseppe marked this conversation as resolved.
Show resolved Hide resolved
* Azure Web PubSub

## Code of Conduct
Expand Down
2 changes: 2 additions & 0 deletions internal/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"
"errors"
"fmt"
"github.com/Azure/azqr/internal/scanners/vgw"
cmendible marked this conversation as resolved.
Show resolved Hide resolved
"sync"
"time"

Expand Down Expand Up @@ -481,6 +482,7 @@ func GetScanners() []scanners.IAzureScanner {
&vm.VirtualMachineScanner{},
&vmss.VirtualMachineScaleSetScanner{},
&vnet.VirtualNetworkScanner{},
&vgw.VirtualNetworkGatewayScanner{},
&wps.WebPubSubScanner{},
}
}
79 changes: 79 additions & 0 deletions internal/scanners/vgw/rules.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// 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",
},
}
}
114 changes: 114 additions & 0 deletions internal/scanners/vgw/rules_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// 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%",
},
},
}
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)
}
})
}
}
64 changes: 64 additions & 0 deletions internal/scanners/vgw/vgw.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading