From a8850b7a0af78004e27fdcd07bd2f4713e4ae2c8 Mon Sep 17 00:00:00 2001 From: Benjamin Engeset <99641908+BenjaminEngeset@users.noreply.github.com> Date: Mon, 5 Aug 2024 17:34:44 +0200 Subject: [PATCH] Updated Azure.VNET.UseNSGs (#3008) * Updated Azure.VNET.UseNSGs * Updated Azure.VNET.UseNSGs * Fix --------- Co-authored-by: Bernie White --- docs/CHANGELOG-v1.md | 4 ++ .../rules/Azure.VNET.Rule.ps1 | 6 +- .../Azure.VNET.Tests.ps1 | 61 ++++++++++++++++--- .../Resources.VirtualNetwork.json | 50 +++++++++++++++ 4 files changed, 108 insertions(+), 13 deletions(-) diff --git a/docs/CHANGELOG-v1.md b/docs/CHANGELOG-v1.md index e96e419d421..cd062e9f688 100644 --- a/docs/CHANGELOG-v1.md +++ b/docs/CHANGELOG-v1.md @@ -33,6 +33,10 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers - Azure Kubernetes Service: - Verify that clusters have the customer-controlled maintenance windows 'aksManagedAutoUpgradeSchedule' and 'aksManagedNodeOSUpgradeSchedule' configured by @BenjaminEngeset. [#2444](https://github.com/Azure/PSRule.Rules.Azure/issues/2444) +- Updated rules: + - Virtual Network: + - Updated `Azure.VNET.UseNSGs` to correctly handle cases for special purpose and customer-excluded subnets by @BenjaminEngeset. + [#3007](https://github.com/Azure/PSRule.Rules.Azure/issues/3007) What's changed since pre-release v1.39.0-B0009: diff --git a/src/PSRule.Rules.Azure/rules/Azure.VNET.Rule.ps1 b/src/PSRule.Rules.Azure/rules/Azure.VNET.Rule.ps1 index 9bd02f580f6..c7e8c180889 100644 --- a/src/PSRule.Rules.Azure/rules/Azure.VNET.Rule.ps1 +++ b/src/PSRule.Rules.Azure/rules/Azure.VNET.Rule.ps1 @@ -15,14 +15,14 @@ Rule 'Azure.VNET.UseNSGs' -Ref 'AZR-000263' -Type 'Microsoft.Network/virtualNetw if ($PSRule.TargetType -eq 'Microsoft.Network/virtualNetworks') { # Get subnets $subnet = @($TargetObject.properties.subnets | Where-Object { - $_.Name -notin $excludedSubnets -and $_.Name -notin $customExcludedSubnets -and @($_.properties.delegations | Where-Object { $_.properties.serviceName -eq 'Microsoft.HardwareSecurityModules/dedicatedHSMs' }).Length -eq 0 + [PSRule.Rules.Azure.Runtime.Helper]::GetSubResourceName($_.Name) -notin $excludedSubnets -and [PSRule.Rules.Azure.Runtime.Helper]::GetSubResourceName($_.Name) -notin $customExcludedSubnets -and @($_.properties.delegations | Where-Object { $_.properties.serviceName -eq 'Microsoft.HardwareSecurityModules/dedicatedHSMs' }).Length -eq 0 }); if ($subnet.Length -eq 0 -or !$Assert.HasFieldValue($TargetObject, 'properties.subnets').Result) { return $Assert.Pass(); } } elseif ($PSRule.TargetType -eq 'Microsoft.Network/virtualNetworks/subnets' -and - ($PSRule.TargetName -in $excludedSubnets -or $PSRule.TargetName -in $customExcludedSubnets -or @($TargetObject.properties.delegations | Where-Object { $_.properties.serviceName -eq 'Microsoft.HardwareSecurityModules/dedicatedHSMs' }).Length -gt 0)) { + ([PSRule.Rules.Azure.Runtime.Helper]::GetSubResourceName($PSRule.TargetName) -in $excludedSubnets -or [PSRule.Rules.Azure.Runtime.Helper]::GetSubResourceName($PSRule.TargetName) -in $customExcludedSubnets -or @($TargetObject.properties.delegations | Where-Object { $_.properties.serviceName -eq 'Microsoft.HardwareSecurityModules/dedicatedHSMs' }).Length -gt 0)) { return $Assert.Pass(); } foreach ($sn in $subnet) { @@ -32,8 +32,6 @@ Rule 'Azure.VNET.UseNSGs' -Ref 'AZR-000263' -Type 'Microsoft.Network/virtualNetw } } -# TODO: Check that NSG on GatewaySubnet is not defined - # Synopsis: VNETs should have at least two DNS servers assigned. Rule 'Azure.VNET.SingleDNS' -Ref 'AZR-000264' -Type 'Microsoft.Network/virtualNetworks' -Tag @{ release = 'GA'; ruleSet = '2020_06'; 'Azure.WAF/pillar' = 'Reliability'; } { # If DNS servers are customized, at least two IP addresses should be defined diff --git a/tests/PSRule.Rules.Azure.Tests/Azure.VNET.Tests.ps1 b/tests/PSRule.Rules.Azure.Tests/Azure.VNET.Tests.ps1 index 823fd65c3d8..20ad452c01c 100644 --- a/tests/PSRule.Rules.Azure.Tests/Azure.VNET.Tests.ps1 +++ b/tests/PSRule.Rules.Azure.Tests/Azure.VNET.Tests.ps1 @@ -31,9 +31,6 @@ Describe 'Azure.VNET' -Tag 'Network', 'VNET' { Module = 'PSRule.Rules.Azure' WarningAction = 'Ignore' ErrorAction = 'Stop' - Option = @{ - 'Configuration.AZURE_VNET_SUBNET_EXCLUDED_FROM_NSG' = @('subnet-ZZ') - } } $dataPath = Join-Path -Path $here -ChildPath 'Resources.VirtualNetwork.json'; $result = Invoke-PSRule @invokeParams -InputPath $dataPath -Outcome All; @@ -45,8 +42,8 @@ Describe 'Azure.VNET' -Tag 'Network', 'VNET' { # Fail $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' }); $ruleResult | Should -Not -BeNullOrEmpty; - $ruleResult.Length | Should -Be 3; - $ruleResult.TargetName | Should -BeIn 'vnet-B', 'vnet-C', 'vnet-D'; + $ruleResult.Length | Should -Be 5; + $ruleResult.TargetName | Should -BeIn 'vnet-B', 'vnet-C', 'vnet-D', 'vnet-G', 'vnet-H/excludedSubnet'; $ruleResult[0].Reason | Should -Not -BeNullOrEmpty; $ruleResult[0].Reason | Should -HaveCount 4; @@ -70,12 +67,18 @@ Describe 'Azure.VNET' -Tag 'Network', 'VNET' { "The subnet (subnet-C) has no NSG associated.", "The subnet (subnet-D) has no NSG associated." ); + $ruleResult[3].Reason | Should -Not -BeNullOrEmpty; + $ruleResult[3].Reason | Should -HaveCount 1; + $ruleResult[3].Reason | Should -Be "The subnet (subnet-ZZ) has no NSG associated."; + $ruleResult[4].Reason | Should -Not -BeNullOrEmpty; + $ruleResult[4].Reason | Should -HaveCount 1; + $ruleResult[4].Reason | Should -Be "The subnet (vnet-H/excludedSubnet) has no NSG associated."; # Pass $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); $ruleResult | Should -Not -BeNullOrEmpty; $ruleResult.Length | Should -Be 4; - $ruleResult.TargetName | Should -BeIn 'vnet-A', 'vnet-E', 'vnet-F', 'vnet-G'; + $ruleResult.TargetName | Should -BeIn 'vnet-A', 'vnet-E', 'vnet-F', 'vnet-H/AzureFirewallSubnet'; } It 'Azure.VNET.SingleDNS' { @@ -156,8 +159,8 @@ Describe 'Azure.VNET' -Tag 'Network', 'VNET' { # Pass $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); $ruleResult | Should -Not -BeNullOrEmpty; - $ruleResult.Length | Should -Be 7; - $ruleResult.TargetName | Should -BeIn 'vnet-A', 'vnet-B', 'vnet-C', 'vnet-D', 'vnet-E', 'vnet-F', 'vnet-G'; + $ruleResult.Length | Should -Be 9; + $ruleResult.TargetName | Should -BeIn 'vnet-A', 'vnet-B', 'vnet-C', 'vnet-D', 'vnet-E', 'vnet-F', 'vnet-G', 'vnet-H/AzureFirewallSubnet', 'vnet-H/excludedSubnet'; } It 'Azure.VNET.BastionSubnet' { @@ -387,10 +390,11 @@ Describe 'Azure.VNET' -Tag 'Network', 'VNET' { ErrorAction = 'Stop' Option = @{ 'Configuration.AZURE_VNET_DNS_WITH_IDENTITY' = $true + 'Configuration.AZURE_VNET_SUBNET_EXCLUDED_FROM_NSG' = @('subnet-ZZ', 'excludedSubnet') } } $dataPath = Join-Path -Path $here -ChildPath 'Resources.VirtualNetwork.json'; - $result = Invoke-PSRule @invokeParams -InputPath $dataPath -Outcome All -Name 'Azure.VNET.LocalDNS'; + $result = Invoke-PSRule @invokeParams -InputPath $dataPath -Outcome All -Name 'Azure.VNET.LocalDNS', 'Azure.VNET.UseNSGs'; } It 'Azure.VNET.LocalDNS' { @@ -404,5 +408,44 @@ Describe 'Azure.VNET' -Tag 'Network', 'VNET' { $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); $ruleResult | Should -BeNullOrEmpty; } + + It 'Azure.VNET.UseNSGs' { + $filteredResult = $result | Where-Object { $_.RuleName -eq 'Azure.VNET.UseNSGs' }; + + # Fail + $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' }); + $ruleResult | Should -Not -BeNullOrEmpty; + $ruleResult.Length | Should -Be 3; + $ruleResult.TargetName | Should -BeIn 'vnet-B', 'vnet-C', 'vnet-D'; + + $ruleResult[0].Reason | Should -Not -BeNullOrEmpty; + $ruleResult[0].Reason | Should -HaveCount 4; + $ruleResult[0].Reason | Should -Be @( + "The subnet (AzureBastionSubnet) has no NSG associated.", + "The subnet (subnet-B) has no NSG associated.", + "The subnet (subnet-C) has no NSG associated.", + "The subnet (subnet-D) has no NSG associated." + ); + $ruleResult[1].Reason | Should -Not -BeNullOrEmpty; + $ruleResult[1].Reason | Should -HaveCount 3; + $ruleResult[1].Reason | Should -Be @( + "The subnet (subnet-B) has no NSG associated.", + "The subnet (subnet-C) has no NSG associated.", + "The subnet (subnet-D) has no NSG associated." + ); + $ruleResult[2].Reason | Should -Not -BeNullOrEmpty; + $ruleResult[2].Reason | Should -HaveCount 3; + $ruleResult[2].Reason | Should -Be @( + "The subnet (subnet-B) has no NSG associated.", + "The subnet (subnet-C) has no NSG associated.", + "The subnet (subnet-D) has no NSG associated." + ); + + # Pass + $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); + $ruleResult | Should -Not -BeNullOrEmpty; + $ruleResult.Length | Should -Be 6; + $ruleResult.TargetName | Should -BeIn 'vnet-A', 'vnet-E', 'vnet-F', 'vnet-G', 'vnet-H/AzureFirewallSubnet', 'vnet-H/excludedSubnet'; + } } } diff --git a/tests/PSRule.Rules.Azure.Tests/Resources.VirtualNetwork.json b/tests/PSRule.Rules.Azure.Tests/Resources.VirtualNetwork.json index a8f3f7ad52c..9bbb6320337 100644 --- a/tests/PSRule.Rules.Azure.Tests/Resources.VirtualNetwork.json +++ b/tests/PSRule.Rules.Azure.Tests/Resources.VirtualNetwork.json @@ -2999,5 +2999,55 @@ } ] } + }, + { + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/vnet-H/subnets/AzureFirewallSubnet", + "Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/vnet-H/subnets/AzureFirewallSubnet", + "Identity": null, + "Kind": null, + "Location": "region", + "ResourceName": "vnet-H/AzureFirewallSubnet", + "Name": "vnet-H/AzureFirewallSubnet", + "Plan": null, + "Properties": { + "addressPrefix": "10.5.0.0/26", + "routeTable": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/routeTables/route-A" + }, + "serviceEndpoints": [], + "delegations": [] + }, + "ResourceGroupName": "test-rg", + "Type": "Microsoft.Network/virtualNetworks/subnets", + "ResourceType": "Microsoft.Network/virtualNetworks/subnets", + "ExtensionResourceType": null, + "Sku": null, + "Tags": null, + "SubscriptionId": "00000000-0000-0000-0000-000000000000" + }, + { + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/vnet-H/subnets/excludedSubnet", + "Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/vnet-H/subnets/excludedSubnet", + "Identity": null, + "Kind": null, + "Location": "region", + "ResourceName": "vnet-H/excludedSubnet", + "Name": "vnet-H/excludedSubnet", + "Plan": null, + "Properties": { + "addressPrefix": "10.0.0.0/29", + "routeTable": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/routeTables/route-A" + }, + "serviceEndpoints": [], + "delegations": [] + }, + "ResourceGroupName": "test-rg", + "Type": "Microsoft.Network/virtualNetworks/subnets", + "ResourceType": "Microsoft.Network/virtualNetworks/subnets", + "ExtensionResourceType": null, + "Sku": null, + "Tags": null, + "SubscriptionId": "00000000-0000-0000-0000-000000000000" } ]