tests/Test-Assessment.25550.ps1

<#
.SYNOPSIS
    Inspection of Outbound TLS Traffic is Enabled on Azure Firewall
.DESCRIPTION
    Verifies that Azure Firewall Premium has TLS inspection enabled by checking for global certificate authority configuration
    and at least one application rule with terminateTLS enabled.
#>


function Test-Assessment-25550 {
    [ZtTest(
        Category = 'Azure Network Security',
        ImplementationCost = 'Low',
        MinimumLicense = ('Azure_Firewall_Premium'),
        Pillar = 'Network',
        RiskLevel = 'High',
        SfiPillar = 'Protect networks',
        TenantType = ('Workforce', 'External'),
        TestId = 25550,
        Title = 'Inspection of Outbound TLS Traffic is Enabled on Azure Firewall',
        UserImpact = 'Low'
    )]
    [CmdletBinding()]
    param()

    Write-PSFMessage '🟦 Start Azure Firewall TLS Inspection evaluation' -Tag Test -Level VeryVerbose

    $activity = 'Checking Azure Firewall TLS Inspection configuration'

    #region Data Collection

    # Check if connected to Azure
    Write-ZtProgress -Activity $activity -Status 'Checking Azure connection'

    $azContext = Get-AzContext -ErrorAction SilentlyContinue
    if (-not $azContext) {
        Write-PSFMessage 'Not connected to Azure.' -Level Warning
        Add-ZtTestResultDetail -SkippedBecause NotConnectedAzure
        return
    }

    # Check the supported environment, 'AzureCloud' in (Get-AzContext).Environment.Name maps to 'Global' in (Get-MgContext).Environment
    Write-ZtProgress -Activity $activity -Status 'Checking Azure environment'

    if ($azContext.Environment.Name -ne 'AzureCloud') {
        Write-PSFMessage "This test is only applicable to the Global (AzureCloud) environment." -Tag Test -Level VeryVerbose
        Add-ZtTestResultDetail -SkippedBecause NotSupported
        return
    }

    Write-ZtProgress -Activity $activity -Status 'Querying Azure subscriptions'
    $subscriptions = Get-AzSubscription
    $resourceManagementUrl = $azContext.Environment.ResourceManagerUrl

    $firewallPoliciesWithTLS = @()

    foreach ($subscription in $subscriptions) {
        Write-ZtProgress -Activity $activity -Status "Checking subscription: $($subscription.Name)"

        # List all firewall policies in subscription (handle possible pagination via nextLink)
        $fwPoliciesUri = "$resourceManagementUrl/subscriptions/$($subscription.Id)/providers/Microsoft.Network/firewallPolicies?api-version=2025-03-01"
        $fwPolicies = @()

        try {
            do {
                $fwPoliciesResp = Invoke-AzRestMethod -Method GET -Uri $fwPoliciesUri

                if ($fwPoliciesResp.StatusCode -eq 403) {
                    Write-PSFMessage "The signed in user does not have access to query firewall policies in subscription $($subscription.Name)." -Level Verbose
                    break
                }

                if ($fwPoliciesResp.StatusCode -ge 400) {
                    throw "Firewall policies request failed with status code $($fwPoliciesResp.StatusCode)"
                }

                $fwPoliciesJson = $fwPoliciesResp.Content | ConvertFrom-Json
                if ($fwPoliciesJson.value) {
                    $fwPolicies += $fwPoliciesJson.value
                }
                $fwPoliciesUri = $fwPoliciesJson.nextLink
            } while ($fwPoliciesUri)
        }
        catch {
            Write-PSFMessage "Unable to list firewall policies in subscription $($subscription.Name): $_" -Tag Test -Level Warning
            continue
        }

        # Filter for Premium SKU policies only
        $premiumPolicies = $fwPolicies | Where-Object { $_.properties.sku.tier -eq 'Premium' }

        foreach ($policy in $premiumPolicies) {
            Write-ZtProgress -Activity $activity -Status "Evaluating policy: $($policy.name)"

            # Get detailed policy configuration
            try {
                $policyDetailResp = Invoke-AzRestMethod -Method GET -Uri "$resourceManagementUrl$($policy.id)?api-version=2025-03-01"
                $policyDetail = $policyDetailResp.Content | ConvertFrom-Json
            }
            catch {
                Write-PSFMessage "Unable to get details for policy $($policy.name): $_" -Tag Test -Level Warning
                continue
            }

            # Check if global TLS certificate is configured
            $tlsGloballyConfigured = $false
            $certName = 'N/A'
            $certKeyVaultSecretId = 'N/A'
            $certKeyVaultSecretIdDisplay = 'N/A'

            $certAuth = $policyDetail.properties.transportSecurity.certificateAuthority
            if ($certAuth.name -and $certAuth.keyVaultSecretId) {
                $tlsGloballyConfigured = $true
                $certName = $certAuth.name
                $certKeyVaultSecretId = $certAuth.keyVaultSecretId
                $certKeyVaultSecretIdDisplay = $certAuth.keyVaultSecretId
            }

            # Check for application rules with terminateTLS enabled
            $tlsEnabledRulesFound = $false
            $ruleCollectionGroups = $policyDetail.properties.ruleCollectionGroups

            foreach ($rcgRef in $ruleCollectionGroups) {
                if ($tlsEnabledRulesFound) { break }

                try {
                    $rcgDetailResp = Invoke-AzRestMethod -Method GET -Uri "$resourceManagementUrl$($rcgRef.id)?api-version=2025-03-01"
                    $rcgDetail = $rcgDetailResp.Content | ConvertFrom-Json
                }
                catch {
                    Write-PSFMessage "Unable to get details for rule collection group $($rcgRef.id): $_" -Tag Test -Level Warning
                    continue
                }

                foreach ($ruleCollection in $rcgDetail.properties.ruleCollections) {
                    if ($tlsEnabledRulesFound) { break }
                    if ($ruleCollection.ruleCollectionType -ne 'FirewallPolicyFilterRuleCollection') { continue }

                    foreach ($rule in $ruleCollection.rules) {
                        if ($rule.ruleType -eq 'ApplicationRule' -and $rule.terminateTLS -eq $true) {
                            $tlsEnabledRulesFound = $true
                            break
                        }
                    }
                }
            }

            # Parse policy ID to extract components for portal URL
            # Format: /subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/{provider}/{resourceType}/{resourceName}
            $policyIdParts = $policy.id -split '/'
            $subscriptionId = $policyIdParts[2]
            $resourceGroupName = $policyIdParts[4]
            $resourceName = $policyIdParts[-1]

            # Create Azure portal URL for the firewall policy
            $portalUrl = "https://portal.azure.com/#@/resource$($policy.id)"

            # Store results
            $firewallPoliciesWithTLS += [PSCustomObject]@{
                SubscriptionId                     = $subscription.Id
                SubscriptionName                   = $subscription.Name
                PolicyName                         = $policy.name
                PolicyId                           = $policy.id
                PortalUrl                          = $portalUrl
                TLSGloballyConfigured              = if ($tlsGloballyConfigured) { 'Yes' } else { 'No' }
                CertificateAuthorityName           = $certName
                CertificateKeyVaultSecretId        = $certKeyVaultSecretId
                CertificateKeyVaultSecretIdDisplay  = $certKeyVaultSecretIdDisplay
                ApplicationRuleWithTLS             = if ($tlsEnabledRulesFound) { 'Yes' } else { 'No' }
                PassesCriteria                     = $tlsGloballyConfigured -and $tlsEnabledRulesFound
            }
        }
    }

    #endregion Data Collection

    #region Assessment Logic

    # Determine pass/fail
    $passed = $false
    $testResultMarkdown = ''

    if ($firewallPoliciesWithTLS.Count -eq 0) {
        $testResultMarkdown = "❌ No Azure Firewall Premium policies found in any subscription.`n`n"
    }
    elseif (($firewallPoliciesWithTLS | Where-Object { $_.PassesCriteria }).Count -gt 0) {
        $passed = $true
        $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%"
    }
    else {
        $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%"
    }

    #endregion Assessment Logic

    #region Report Generation
    $formatTemplate = @'
 
## Azure Firewall policies TLS inspection status
 
| 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 |
| :------------- | :------------------------- | :--------------------------------- | :------------------------- | :------------------------------------- | :------------------------------------------- |
{0}
 
'@


    $tableRows = ''
    foreach ($policyInfo in $firewallPoliciesWithTLS) {
        $policyName = Get-SafeMarkdown -Text $policyInfo.PolicyName
        $portalUrl = $policyInfo.PortalUrl
        $policyNameWithLink = "[$policyName]($portalUrl)"
        $subName = Get-SafeMarkdown -Text $policyInfo.SubscriptionName
        $certName = Get-SafeMarkdown -Text $policyInfo.CertificateAuthorityName
        $certKeyVault = Get-SafeMarkdown -Text $policyInfo.CertificateKeyVaultSecretIdDisplay

        $tableRows += "| $subName | $policyNameWithLink | $($policyInfo.TLSGloballyConfigured) | $certName | $certKeyVault | $($policyInfo.ApplicationRuleWithTLS) |`n"
    }

    $mdInfo = $formatTemplate -f $tableRows
    $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo

    #endregion Report Generation

    $params = @{
        TestId = '25550'
        Status = $passed
        Result = $testResultMarkdown
    }

    Add-ZtTestResultDetail @params
}