tests/Test-Assessment.27004.ps1

<#
.SYNOPSIS
    Validates that custom TLS inspection bypass rules do not duplicate system bypass destinations.
 
.DESCRIPTION
    This test checks whether custom TLS inspection bypass rules contain destinations that are
    already covered by Microsoft's system bypass list. Redundant rules:
    - Consume policy capacity unnecessarily
    - Create administrative overhead
    - May cause confusion about necessary vs. duplicated rules
 
    The test identifies exact matches, subdomain matches, and wildcard overlaps between
    custom bypass rules and the system bypass list.
 
.NOTES
    Test ID: 27004
    Category: Global Secure Access
    Required API: networkAccess/tlsInspectionPolicies (beta) with $expand=policyRules
    System Bypass List: assets/27004-system-bypass-fqdns.json (sourced from GSA backend team; manually maintained until API is available)
#>


function Test-Assessment-27004 {
    [ZtTest(
        Category = 'Global Secure Access',
        ImplementationCost = 'Low',
        MinimumLicense = ('Entra_Premium_Internet_Access'),
        Pillar = 'Network',
        RiskLevel = 'Low',
        SfiPillar = 'Protect networks',
        TenantType = ('Workforce'),
        TestId = 27004,
        Title = 'TLS inspection custom bypass rules do not duplicate system bypass destinations',
        UserImpact = 'Low'
    )]
    [CmdletBinding()]
    param()

    # Constants for output display limits
    [int]$MAX_RULES_DISPLAYED = 10
    [int]$MAX_RULE_GROUPS = 10
    [int]$MAX_DESTINATIONS_PER_RULE = 10

    #region Data Collection
    Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose

    $activity = 'Checking TLS inspection bypass rules for redundant system destinations'
    Write-ZtProgress -Activity $activity -Status 'Loading system bypass reference list'

    # Load system bypass FQDN list from config file.
    $dataFilePath = Join-Path $PSScriptRoot '..' 'assets' '27004-system-bypass-fqdns.json' | Resolve-Path -ErrorAction SilentlyContinue
    if (-not $dataFilePath -or -not (Test-Path $dataFilePath)) {
        Write-PSFMessage "System bypass FQDN config file not found: $dataFilePath" -Tag Test -Level Warning
        Add-ZtTestResultDetail -SkippedBecause NotSupported
        return
    }

    $jsonErrorMsg = $null
    try {
        $bypassConfig = Get-Content $dataFilePath -Raw | ConvertFrom-Json
        $systemFqdns = @($bypassConfig.fqdns)
        $systemFqdnsLower = $systemFqdns | ForEach-Object { $_.ToLower() }
        Write-PSFMessage "Loaded $($systemFqdns.Count) system bypass FQDNs from config (last updated: $($bypassConfig.metadata.lastUpdated))" -Tag Test -Level VeryVerbose
    }
    catch {
        $jsonErrorMsg = $_
        Write-PSFMessage "Failed to parse system bypass config file: $jsonErrorMsg" -Tag Test -Level Warning
    }

    # System recommended bypass categories (from priority 65000 rule)
    $systemCategories = @('Education', 'Finance', 'Government', 'HealthAndMedicine')
    $systemCategoriesLower = $systemCategories | ForEach-Object { $_.ToLower() }

    Write-ZtProgress -Activity $activity -Status 'Querying TLS inspection policies and rules'

    $tlsPolicies = @()
    $errorMsg = $null

    try {
        $tlsPolicies = Invoke-ZtGraphRequest `
            -RelativeUri 'networkAccess/tlsInspectionPolicies' `
            -QueryParameters @{ '$expand' = 'policyRules' } `
            -ApiVersion beta
    }
    catch {
        $errorMsg = $_
        Write-PSFMessage "Failed to retrieve TLS inspection policies: $errorMsg" -Tag Test -Level Warning
    }
    #endregion Data Collection

    #region Assessment Logic
    $testResultMarkdown = ''
    $passed = $false
    $customStatus = $null

    if ($jsonErrorMsg) {
        # JSON parsing failed - unable to load system bypass configuration
        $passed = $false
        $customStatus = 'Investigate'
        $testResultMarkdown = "⚠️ Unable to load system bypass configuration due to JSON parsing error.`n`n%TestResult%"
    }
    elseif ($errorMsg) {
        # API call failed - unable to determine status
        $passed = $false
        $customStatus = 'Investigate'
        $testResultMarkdown = "⚠️ Unable to retrieve TLS inspection policies due to API error or insufficient permissions.`n`n%TestResult%"
    }
    elseif ($null -eq $tlsPolicies -or $tlsPolicies.Count -eq 0) {
        # No TLS inspection policies configured - prerequisite not met
        Write-PSFMessage 'TLS inspection is not configured in this tenant.' -Tag Test -Level Verbose
        Add-ZtTestResultDetail -SkippedBecause NotApplicable -Result 'TLS inspection is not configured in this tenant. This check is not applicable until a TLS inspection policy is created.'
        return
    }
    else {

    Write-ZtProgress -Activity $activity -Status 'Analyzing bypass rules for redundancies'

    $allBypassRules = [System.Collections.Generic.List[object]]::new()
    $redundantRules = [System.Collections.Generic.List[object]]::new()

    foreach ($policy in $tlsPolicies) {
        if ($null -eq $policy.policyRules) {
            continue
        }

        $bypassRules = @($policy.policyRules | Where-Object { $_.action -eq 'bypass' })

        foreach ($rule in $bypassRules) {
            # Skip auto-created system rules
            if ($rule.description -like 'Auto-created TLS rule*') {
                continue
            }

            $destinations = @()
            $destinationTypeMap = @{}
            $matchedPairs = [System.Collections.Generic.List[object]]::new()

            # Extract destinations from matchingConditions, tracking type per value
            if ($null -ne $rule.matchingConditions -and $null -ne $rule.matchingConditions.destinations) {
                foreach ($dest in $rule.matchingConditions.destinations) {
                    if ($null -ne $dest.values) {
                        $destType = if ($dest.'@odata.type' -like '*tlsInspectionFqdnDestination*') { 'FQDN' }
                                    elseif ($dest.'@odata.type' -like '*tlsInspectionWebCategoryDestination*') { 'Category' }
                                    else { 'Unknown' }
                        foreach ($v in $dest.values) {
                            $destinations += $v
                            $destinationTypeMap[$v] = $destType
                        }
                    }
                }
            }

            # Skip rule if no destinations found
            if ($destinations.Count -eq 0) {
                continue
            }

            # Check each custom destination against the system bypass list.
            # Supports exact matches, subdomain matches under wildcards (*.domain.com),
            # wildcard-to-wildcard matches, and double-wildcard patterns (*.domain.*).
            foreach ($destination in $destinations) {
                $destLower = $destination.ToLower().Trim()
                $destType = if ($destinationTypeMap.ContainsKey($destination)) { $destinationTypeMap[$destination] } else { 'FQDN' }

                # Check if this is a web category destination
                if ($destType -eq 'Category') {
                    # Check against system recommended bypass categories
                    for ($i = 0; $i -lt $systemCategoriesLower.Count; $i++) {
                        if ($destLower -eq $systemCategoriesLower[$i]) {
                            $matchedPairs.Add([PSCustomObject]@{
                                CustomFqdn = $destination
                                SystemFqdn = $systemCategories[$i]
                                MatchType  = 'Exact'
                                DestType   = 'Category'
                            })
                            break
                        }
                    }
                }
                else {
                    # Check FQDN against system bypass FQDN list
                    for ($i = 0; $i -lt $systemFqdnsLower.Count; $i++) {
                        $sysFqdn = $systemFqdnsLower[$i]
                        $isMatch = $false
                        $matchType = ''

                        if ($destLower -eq $sysFqdn) {
                            # Exact match (covers wildcard-to-wildcard too)
                            $isMatch = $true; $matchType = 'Exact'
                        }
                        elseif ($sysFqdn -match '^\*\.([^.]+)\.\*$') {
                            # Double-wildcard: *.domain.* — check if custom destination matches the pattern
                            $mid = [regex]::Escape($Matches[1])
                            if ($destLower -match "\.$mid\.") {
                                $isMatch = $true
                                # Determine match type based on whether custom is also a wildcard
                                $matchType = if ($destLower -match '^\*\.') { 'Wildcard' } else { 'Subdomain' }
                            }
                        }
                        elseif ($sysFqdn -match '^\*\.(.+)$') {
                            # Standard wildcard: *.domain.com
                            $suffix = $Matches[1]
                            if ($destLower -eq "*.$suffix") {
                                # Exact wildcard-to-wildcard match
                                $isMatch = $true
                                $matchType = 'Exact'
                            }
                            elseif ($destLower -like "*.$suffix") {
                                # Subdomain of the wildcard suffix
                                $isMatch = $true
                                $matchType = 'Subdomain'
                            }
                            elseif ($destLower -eq $suffix) {
                                # Base domain match
                                $isMatch = $true
                                $matchType = 'Subdomain'
                            }
                        }

                        # Check if custom destination is a wildcard being covered by system base domain
                        if (-not $isMatch -and $destLower -match '^\*\.(.+)$') {
                            $customSuffix = $Matches[1]
                            if ($sysFqdn -eq $customSuffix -or $sysFqdn -eq "*.$customSuffix") {
                                $isMatch = $true; $matchType = 'Wildcard'
                            }
                        }

                        if ($isMatch) {
                            $matchedPairs.Add([PSCustomObject]@{
                                CustomFqdn = $destination
                                SystemFqdn = $systemFqdns[$i]
                                MatchType  = $matchType
                                DestType   = 'FQDN'
                            })
                            break  # Only match once per destination
                        }
                    }
                }
            }

            $ruleStatus = if ($matchedPairs.Count -eq 0) { 'No Overlap' }
                          elseif ($matchedPairs.Count -ge $destinations.Count) { 'Redundant' }
                          else { 'Partial' }

            $ruleInfo = [PSCustomObject]@{
                PolicyName        = $policy.name
                PolicyId          = $policy.id
                RuleName          = $rule.name
                RuleId            = $rule.id
                Destinations      = $destinations
                TotalDestinations = $destinations.Count
                RedundantCount    = $matchedPairs.Count
                MatchedPairs      = $matchedPairs
                Status            = $ruleStatus
            }

            $allBypassRules.Add($ruleInfo)

            if ($ruleStatus -ne 'No Overlap') {
                $redundantRules.Add($ruleInfo)
            }
        }
    }

        # Evaluate test result per spec evaluation logic
        if ($redundantRules.Count -eq 0) {
            # No custom bypass rules OR custom rules exist but none are redundant - pass
            $passed = $true
            $testResultMarkdown = "✅ All custom TLS inspection bypass rules target unique destinations not covered by the system bypass list.`n`n%TestResult%"
        }
        else {
            # Any matches found - fail with list of redundant rules
            $passed = $false
            $testResultMarkdown = "❌ Found custom bypass rules that duplicate system bypass destinations; these rules are redundant and can be removed to simplify policy management.`n`n%TestResult%"
        }
    }
    #endregion Assessment Logic

    #region Report Generation
    $mdInfo = ''

    if ($allBypassRules.Count -gt 0) {
        $reportTitle = 'TLS Inspection Bypass Rule Analysis'
        $portalLink = 'https://entra.microsoft.com/#view/Microsoft_Azure_Network_Access/TLSInspectionPolicy.ReactView'

        # Calculate totals
        $totalDestinations = ($allBypassRules | ForEach-Object { $_.TotalDestinations } | Measure-Object -Sum).Sum
        $totalRedundantDestinations = ($allBypassRules | ForEach-Object { $_.RedundantCount } | Measure-Object -Sum).Sum
        $totalUniqueDestinations = $totalDestinations - $totalRedundantDestinations

        # Sort rules per spec: Status priority (Redundant → Partial → No Overlap), then by descending redundant count
        $statusPriority = @{ 'Redundant' = 1; 'Partial' = 2; 'No Overlap' = 3 }
        $sortedRules = $allBypassRules | Sort-Object { $statusPriority[$_.Status] }, @{ Expression = { $_.RedundantCount }; Descending = $true }

        # Build rule-level summary table with row cap
        $rulesTable = "#### Rule-level summary`n`n"
        $rulesTable += "| Policy name | Rule name | Total destinations | Redundant destinations | Status |`n"
        $rulesTable += "| :---------- | :-------- | :----------------- | :--------------------- | :----- |`n"

        $displayedRules = $sortedRules | Select-Object -First $MAX_RULES_DISPLAYED

        foreach ($rule in $displayedRules) {
            $policyName = Get-SafeMarkdown -Text $rule.PolicyName
            $ruleName = Get-SafeMarkdown -Text $rule.RuleName
            $rulesTable += "| $policyName | $ruleName | $($rule.TotalDestinations) | $($rule.RedundantCount) | $($rule.Status) |`n"
        }

        # Add overflow summary if there are more rules than the display limit
        if ($sortedRules.Count -gt $MAX_RULES_DISPLAYED) {
            $remaining = $sortedRules | Select-Object -Skip $MAX_RULES_DISPLAYED
            $remainingCount = $remaining.Count
            $remainingRedundant = ($remaining | Where-Object { $_.Status -eq 'Redundant' }).Count
            $remainingPartial = ($remaining | Where-Object { $_.Status -eq 'Partial' }).Count
            $remainingNoOverlap = ($remaining | Where-Object { $_.Status -eq 'No Overlap' }).Count
            $rulesTable += "| *+ $remainingCount more rules not shown ($remainingRedundant redundant, $remainingPartial partial, $remainingNoOverlap no overlap)* | | | | |`n"
        }

        # Build redundant destination detail grouped by rule
        $redundantDetail = ''
        if ($redundantRules.Count -gt 0) {
            $redundantDetail = "#### Redundant destination detail`n`n"

            # Sort redundant rules by same criteria: Status priority then redundant count desc
            $sortedRedundantRules = $redundantRules | Sort-Object { $statusPriority[$_.Status] }, @{ Expression = { $_.RedundantCount }; Descending = $true }

            # Cap at maximum rule groups
            $displayedRuleGroups = $sortedRedundantRules | Select-Object -First $MAX_RULE_GROUPS

            foreach ($rule in $displayedRuleGroups) {
                $policyName = Get-SafeMarkdown -Text $rule.PolicyName
                $ruleName = Get-SafeMarkdown -Text $rule.RuleName
                $redundantDetail += "**Rule: $ruleName** (Policy: $policyName) — $($rule.RedundantCount) of $($rule.TotalDestinations) destinations redundant`n`n"
                $redundantDetail += "| # | Custom bypass destination | Destination type | Matched system bypass entry | Match type |`n"
                $redundantDetail += "| :- | :----------------------- | :--------------- | :-------------------------- | :--------- |`n"

                # Cap at maximum destination entries per rule group
                $displayedPairs = $rule.MatchedPairs | Select-Object -First $MAX_DESTINATIONS_PER_RULE

                $rowNum = 1
                foreach ($pair in $displayedPairs) {
                    # Escape asterisks in FQDNs for markdown rendering (prevent italic/bold interpretation)
                    $customFqdn = $pair.CustomFqdn -replace '\*', '\*'
                    $systemFqdn = $pair.SystemFqdn -replace '\*', '\*'
                    $redundantDetail += "| $rowNum | $customFqdn | $($pair.DestType) | $systemFqdn | $($pair.MatchType) |`n"
                    $rowNum++
                }

                # Add overflow row if this rule has more redundant destinations than display limit
                if ($rule.MatchedPairs.Count -gt $MAX_DESTINATIONS_PER_RULE) {
                    $remainingPairs = $rule.MatchedPairs.Count - $MAX_DESTINATIONS_PER_RULE
                    $redundantDetail += "| | *+ $remainingPairs more redundant destinations not shown for this rule* | | | |`n"
                }

                $redundantDetail += "`n"
            }

            # Add overflow line if there are more rule groups than display limit
            if ($sortedRedundantRules.Count -gt $MAX_RULE_GROUPS) {
                $remainingRuleGroups = $sortedRedundantRules.Count - $MAX_RULE_GROUPS
                $redundantDetail += "*+ $remainingRuleGroups more rules with redundant destinations not shown*`n`n"
            }
        }

        $formatTemplate = @'
 
## [{0}]({1})
 
**Overview:**
- Total custom bypass rules: {2}
- Total custom bypass destinations: {3}
- Redundant destinations found: {4}
- Unique destinations: {5}
 
{6}
 
{7}
'@


        $mdInfo = $formatTemplate -f $reportTitle, $portalLink, $allBypassRules.Count, $totalDestinations, $totalRedundantDestinations, $totalUniqueDestinations, $rulesTable, $redundantDetail
    }

    $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo
    #endregion Report Generation

    $params = @{
        TestId = '27004'
        Title  = 'TLS inspection custom bypass rules do not duplicate system bypass destinations'
        Status = $passed
        Result = $testResultMarkdown
    }
    if ($customStatus) {
        $params.CustomStatus = $customStatus
    }
    Add-ZtTestResultDetail @params
}