|
| 1 | +<# |
| 2 | +.SYNOPSIS |
| 3 | + Inspection of Outbound TLS Traffic is Enabled on Azure Firewall |
| 4 | +.DESCRIPTION |
| 5 | + Verifies that Azure Firewall Premium has TLS inspection enabled by checking for global certificate authority configuration |
| 6 | + and at least one application rule with terminateTLS enabled. |
| 7 | +#> |
| 8 | + |
| 9 | +function Test-Assessment-25550 { |
| 10 | + [ZtTest( |
| 11 | + Category = 'Azure Network Security', |
| 12 | + ImplementationCost = 'Low', |
| 13 | + MinimumLicense = ('Azure_Firewall_Premium'), |
| 14 | + Pillar = 'Network', |
| 15 | + RiskLevel = 'High', |
| 16 | + SfiPillar = 'Protect networks', |
| 17 | + TenantType = ('Workforce', 'External'), |
| 18 | + TestId = 25550, |
| 19 | + Title = 'Inspection of Outbound TLS Traffic is Enabled on Azure Firewall', |
| 20 | + UserImpact = 'Low' |
| 21 | + )] |
| 22 | + [CmdletBinding()] |
| 23 | + param() |
| 24 | + |
| 25 | + Write-PSFMessage '🟦 Start Azure Firewall TLS Inspection evaluation' -Tag Test -Level VeryVerbose |
| 26 | + |
| 27 | + $activity = 'Checking Azure Firewall TLS Inspection configuration' |
| 28 | + |
| 29 | + #region Data Collection |
| 30 | + |
| 31 | + # Check if connected to Azure |
| 32 | + Write-ZtProgress -Activity $activity -Status 'Checking Azure connection' |
| 33 | + |
| 34 | + $azContext = Get-AzContext -ErrorAction SilentlyContinue |
| 35 | + if (-not $azContext) { |
| 36 | + Write-PSFMessage 'Not connected to Azure.' -Level Warning |
| 37 | + Add-ZtTestResultDetail -SkippedBecause NotConnectedAzure |
| 38 | + return |
| 39 | + } |
| 40 | + |
| 41 | + # Check the supported environment, 'AzureCloud' in (Get-AzContext).Environment.Name maps to 'Global' in (Get-MgContext).Environment |
| 42 | + Write-ZtProgress -Activity $activity -Status 'Checking Azure environment' |
| 43 | + |
| 44 | + if ($azContext.Environment.Name -ne 'AzureCloud') { |
| 45 | + Write-PSFMessage "This test is only applicable to the Global (AzureCloud) environment." -Tag Test -Level VeryVerbose |
| 46 | + Add-ZtTestResultDetail -SkippedBecause NotSupported |
| 47 | + return |
| 48 | + } |
| 49 | + |
| 50 | + Write-ZtProgress -Activity $activity -Status 'Querying Azure subscriptions' |
| 51 | + $subscriptions = Get-AzSubscription |
| 52 | + $resourceManagementUrl = $azContext.Environment.ResourceManagerUrl |
| 53 | + |
| 54 | + $firewallPoliciesWithTLS = @() |
| 55 | + |
| 56 | + foreach ($subscription in $subscriptions) { |
| 57 | + Write-ZtProgress -Activity $activity -Status "Checking subscription: $($subscription.Name)" |
| 58 | + |
| 59 | + # List all firewall policies in subscription (handle possible pagination via nextLink) |
| 60 | + $fwPoliciesUri = "$resourceManagementUrl/subscriptions/$($subscription.Id)/providers/Microsoft.Network/firewallPolicies?api-version=2025-03-01" |
| 61 | + $fwPolicies = @() |
| 62 | + |
| 63 | + try { |
| 64 | + do { |
| 65 | + $fwPoliciesResp = Invoke-AzRestMethod -Method GET -Uri $fwPoliciesUri |
| 66 | + |
| 67 | + if ($fwPoliciesResp.StatusCode -eq 403) { |
| 68 | + Write-PSFMessage "The signed in user does not have access to query firewall policies in subscription $($subscription.Name)." -Level Verbose |
| 69 | + break |
| 70 | + } |
| 71 | + |
| 72 | + if ($fwPoliciesResp.StatusCode -ge 400) { |
| 73 | + throw "Firewall policies request failed with status code $($fwPoliciesResp.StatusCode)" |
| 74 | + } |
| 75 | + |
| 76 | + $fwPoliciesJson = $fwPoliciesResp.Content | ConvertFrom-Json |
| 77 | + if ($fwPoliciesJson.value) { |
| 78 | + $fwPolicies += $fwPoliciesJson.value |
| 79 | + } |
| 80 | + $fwPoliciesUri = $fwPoliciesJson.nextLink |
| 81 | + } while ($fwPoliciesUri) |
| 82 | + } |
| 83 | + catch { |
| 84 | + Write-PSFMessage "Unable to list firewall policies in subscription $($subscription.Name): $_" -Tag Test -Level Warning |
| 85 | + continue |
| 86 | + } |
| 87 | + |
| 88 | + # Filter for Premium SKU policies only |
| 89 | + $premiumPolicies = $fwPolicies | Where-Object { $_.properties.sku.tier -eq 'Premium' } |
| 90 | + |
| 91 | + foreach ($policy in $premiumPolicies) { |
| 92 | + Write-ZtProgress -Activity $activity -Status "Evaluating policy: $($policy.name)" |
| 93 | + |
| 94 | + # Get detailed policy configuration |
| 95 | + try { |
| 96 | + $policyDetailResp = Invoke-AzRestMethod -Method GET -Uri "$resourceManagementUrl$($policy.id)?api-version=2025-03-01" |
| 97 | + $policyDetail = $policyDetailResp.Content | ConvertFrom-Json |
| 98 | + } |
| 99 | + catch { |
| 100 | + Write-PSFMessage "Unable to get details for policy $($policy.name): $_" -Tag Test -Level Warning |
| 101 | + continue |
| 102 | + } |
| 103 | + |
| 104 | + # Check if global TLS certificate is configured |
| 105 | + $tlsGloballyConfigured = $false |
| 106 | + $certName = 'N/A' |
| 107 | + $certKeyVaultSecretId = 'N/A' |
| 108 | + $certKeyVaultSecretIdDisplay = 'N/A' |
| 109 | + |
| 110 | + $certAuth = $policyDetail.properties.transportSecurity.certificateAuthority |
| 111 | + if ($certAuth.name -and $certAuth.keyVaultSecretId) { |
| 112 | + $tlsGloballyConfigured = $true |
| 113 | + $certName = $certAuth.name |
| 114 | + $certKeyVaultSecretId = $certAuth.keyVaultSecretId |
| 115 | + $certKeyVaultSecretIdDisplay = $certAuth.keyVaultSecretId |
| 116 | + } |
| 117 | + |
| 118 | + # Check for application rules with terminateTLS enabled |
| 119 | + $tlsEnabledRulesFound = $false |
| 120 | + $ruleCollectionGroups = $policyDetail.properties.ruleCollectionGroups |
| 121 | + |
| 122 | + foreach ($rcgRef in $ruleCollectionGroups) { |
| 123 | + if ($tlsEnabledRulesFound) { break } |
| 124 | + |
| 125 | + try { |
| 126 | + $rcgDetailResp = Invoke-AzRestMethod -Method GET -Uri "$resourceManagementUrl$($rcgRef.id)?api-version=2025-03-01" |
| 127 | + $rcgDetail = $rcgDetailResp.Content | ConvertFrom-Json |
| 128 | + } |
| 129 | + catch { |
| 130 | + Write-PSFMessage "Unable to get details for rule collection group $($rcgRef.id): $_" -Tag Test -Level Warning |
| 131 | + continue |
| 132 | + } |
| 133 | + |
| 134 | + foreach ($ruleCollection in $rcgDetail.properties.ruleCollections) { |
| 135 | + if ($tlsEnabledRulesFound) { break } |
| 136 | + if ($ruleCollection.ruleCollectionType -ne 'FirewallPolicyFilterRuleCollection') { continue } |
| 137 | + |
| 138 | + foreach ($rule in $ruleCollection.rules) { |
| 139 | + if ($rule.ruleType -eq 'ApplicationRule' -and $rule.terminateTLS -eq $true) { |
| 140 | + $tlsEnabledRulesFound = $true |
| 141 | + break |
| 142 | + } |
| 143 | + } |
| 144 | + } |
| 145 | + } |
| 146 | + |
| 147 | + # Parse policy ID to extract components for portal URL |
| 148 | + # Format: /subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/{provider}/{resourceType}/{resourceName} |
| 149 | + $policyIdParts = $policy.id -split '/' |
| 150 | + $subscriptionId = $policyIdParts[2] |
| 151 | + $resourceGroupName = $policyIdParts[4] |
| 152 | + $resourceName = $policyIdParts[-1] |
| 153 | + |
| 154 | + # Create Azure portal URL for the firewall policy |
| 155 | + $portalUrl = "https://portal.azure.com/#@/resource$($policy.id)" |
| 156 | + |
| 157 | + # Store results |
| 158 | + $firewallPoliciesWithTLS += [PSCustomObject]@{ |
| 159 | + SubscriptionId = $subscription.Id |
| 160 | + SubscriptionName = $subscription.Name |
| 161 | + PolicyName = $policy.name |
| 162 | + PolicyId = $policy.id |
| 163 | + PortalUrl = $portalUrl |
| 164 | + TLSGloballyConfigured = if ($tlsGloballyConfigured) { 'Yes' } else { 'No' } |
| 165 | + CertificateAuthorityName = $certName |
| 166 | + CertificateKeyVaultSecretId = $certKeyVaultSecretId |
| 167 | + CertificateKeyVaultSecretIdDisplay = $certKeyVaultSecretIdDisplay |
| 168 | + ApplicationRuleWithTLS = if ($tlsEnabledRulesFound) { 'Yes' } else { 'No' } |
| 169 | + PassesCriteria = $tlsGloballyConfigured -and $tlsEnabledRulesFound |
| 170 | + } |
| 171 | + } |
| 172 | + } |
| 173 | + |
| 174 | + #endregion Data Collection |
| 175 | + |
| 176 | + #region Assessment Logic |
| 177 | + |
| 178 | + # Determine pass/fail |
| 179 | + $passed = $false |
| 180 | + $testResultMarkdown = '' |
| 181 | + |
| 182 | + if ($firewallPoliciesWithTLS.Count -eq 0) { |
| 183 | + $testResultMarkdown = "❌ No Azure Firewall Premium policies found in any subscription.`n`n" |
| 184 | + } |
| 185 | + elseif (($firewallPoliciesWithTLS | Where-Object { $_.PassesCriteria }).Count -gt 0) { |
| 186 | + $passed = $true |
| 187 | + $testResultMarkdown = "✅ TLS inspection is globally configured in the Azure Firewall policy and at least one application rule explicitly enables TLS inspection with `"terminateTLS: true`".`n`n%TestResult%" |
| 188 | + } |
| 189 | + else { |
| 190 | + $testResultMarkdown = "❌ TLS inspection is not enabled. Either the transportSecurity.certificateAuthority is missing in the firewall policy, or TLS inspection is globally configured but no application rule enables TLS inspection (application rules have `"terminateTLS: false`").`n`n%TestResult%" |
| 191 | + } |
| 192 | + |
| 193 | + #endregion Assessment Logic |
| 194 | + |
| 195 | + #region Report Generation |
| 196 | + $formatTemplate = @' |
| 197 | +
|
| 198 | +## Azure Firewall policies TLS inspection status |
| 199 | +
|
| 200 | +| Subscription name | Azure Firewall policy name | TLS inspection globally configured | Certificate authority name | Certificate authority Key Vault secret ID | Application rule with TLS inspection enabled | |
| 201 | +| :------------- | :------------------------- | :--------------------------------- | :------------------------- | :------------------------------------- | :------------------------------------------- | |
| 202 | +{0} |
| 203 | +
|
| 204 | +'@ |
| 205 | + |
| 206 | + $tableRows = '' |
| 207 | + foreach ($policyInfo in $firewallPoliciesWithTLS) { |
| 208 | + $policyName = Get-SafeMarkdown -Text $policyInfo.PolicyName |
| 209 | + $portalUrl = $policyInfo.PortalUrl |
| 210 | + $policyNameWithLink = "[$policyName]($portalUrl)" |
| 211 | + $subName = Get-SafeMarkdown -Text $policyInfo.SubscriptionName |
| 212 | + $certName = Get-SafeMarkdown -Text $policyInfo.CertificateAuthorityName |
| 213 | + $certKeyVault = Get-SafeMarkdown -Text $policyInfo.CertificateKeyVaultSecretIdDisplay |
| 214 | + |
| 215 | + $tableRows += "| $subName | $policyNameWithLink | $($policyInfo.TLSGloballyConfigured) | $certName | $certKeyVault | $($policyInfo.ApplicationRuleWithTLS) |`n" |
| 216 | + } |
| 217 | + |
| 218 | + $mdInfo = $formatTemplate -f $tableRows |
| 219 | + $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo |
| 220 | + |
| 221 | + #endregion Report Generation |
| 222 | + |
| 223 | + $params = @{ |
| 224 | + TestId = '25550' |
| 225 | + Status = $passed |
| 226 | + Result = $testResultMarkdown |
| 227 | + } |
| 228 | + |
| 229 | + Add-ZtTestResultDetail @params |
| 230 | +} |
0 commit comments