tests/Test-Assessment.25411.ps1

<#
.SYNOPSIS
    TLS inspection is enabled and correctly configured for outbound traffic in Global Secure Access.
.DESCRIPTION
    Verifies that a TLS Inspection policy is properly configured. It will fail if no TLS Inspection policy exists, if the policy is not linked to a Security Profile, or if no Conditional Access policy assigning that Security Profile can be identified.
#>


function Test-Assessment-25411 {
    [ZtTest(
        Category = 'Global Secure Access',
        ImplementationCost = 'High',
        MinimumLicense = ('Entra_Premium_Internet_Access'),
        Pillar = 'Network',
        RiskLevel = 'High',
        SfiPillar = 'Protect networks',
        TenantType = ('Workforce'),
        TestId = 25411,
        Title = 'TLS inspection is enabled and correctly configured for outbound traffic in Global Secure Access',
        UserImpact = 'Medium'
    )]
    [CmdletBinding()]
    param()

    # Define constants
    [int]$BASELINE_PROFILE_PRIORITY = 65000

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

    $activity = 'TLS inspection is enabled and correctly configured for outbound traffic in Global Secure Access.'
    Write-ZtProgress -Activity $activity -Status 'Querying TLS inspection policies'

    # Step 1: Get TLS Inspection policies
    $tlsInspectionPolicies = Invoke-ZtGraphRequest -RelativeUri 'networkAccess/tlsInspectionPolicies' -ApiVersion beta

    # Step 2: List all policies in the Baseline Profile and in each Security Profile
    Write-ZtProgress -Activity $activity -Status 'Querying filtering profiles and policies'
    $filteringProfiles = Invoke-ZtGraphRequest -RelativeUri 'networkAccess/filteringProfiles' -QueryParameters @{
        '$select' = 'id,name,description,state,version,priority'
        '$expand' = 'policies($select=id,state;$expand=policy($select=id,name,version)),conditionalAccessPolicies($select=id,displayName)'
    } -ApiVersion beta

    # Query all Conditional Access policies with details
    Write-ZtProgress -Activity $activity -Status 'Querying Conditional Access policies'
    $allCAPolicies = Get-ZtConditionalAccessPolicy

    # Build CA profile lookup for O(1) access instead of O(N) search per profile
    Write-ZtProgress -Activity $activity -Status 'Building Conditional Access policy lookup'
    $caProfileLookup = @{}
    foreach ($cap in $allCAPolicies) {
        $session = $cap.sessionControls
        if ($null -ne $session -and $null -ne $session.globalSecureAccessFilteringProfile) {
            $sessionProfileId = $session.globalSecureAccessFilteringProfile.profileId
            $sessionEnabled = $session.globalSecureAccessFilteringProfile.isEnabled

            if ($sessionEnabled -eq $true -and $cap.state -eq 'enabled') {
                if (-not $caProfileLookup.ContainsKey($sessionProfileId)) {
                    $caProfileLookup[$sessionProfileId] = @()
                }
                $caProfileLookup[$sessionProfileId] += [PSCustomObject]@{
                    Id          = $cap.id
                    DisplayName = $cap.displayName
                    State       = $cap.state
                }
            }
        }
    }

    #endregion Data Collection

    #region Data Processing
    # Graph responses are automatically unwrapped by Invoke-ZtGraphRequest
    $enabledSecurityProfiles = @()
    $enabledBaseLineProfiles = @()

    # Iterate each TLS inspection policy and find linked profiles
    foreach ($tlsPolicy in $tlsInspectionPolicies) {
        $tlsId = $tlsPolicy.id
        $baseLineProfileFound = $false
        foreach ($profileItem in $filteringProfiles) {
            $profilePolicies = @()
            if ($null -ne $profileItem.policies) {
                $profilePolicies = $profileItem.policies
            }

            foreach ($plink in $profilePolicies) {
                $plinkType = $plink.'@odata.type'
                $linkedPolicyId = $null
                # Only process tlsInspectionPolicyLink entries
                if ($plinkType -eq '#microsoft.graph.networkaccess.tlsInspectionPolicyLink' -and $null -ne $plink.policy) {
                    $linkedPolicyId = $plink.policy.id
                }

                if ($null -ne $linkedPolicyId -and $linkedPolicyId -eq $tlsId) {
                    $linkState = if ($null -ne $plink.state) {
                        $plink.state
                    }
                    else {
                        'unknown'
                    }
                    $profileState = if ($null -ne $profileItem.state) {
                        $profileItem.state
                    }
                    else {
                        'unknown'
                    }
                    $priority = if ($null -ne $profileItem.priority) {
                        [int]$profileItem.priority
                    }
                    else {
                        $null
                    }

                    if ($priority -eq $BASELINE_PROFILE_PRIORITY) {
                        # Baseline Profile: apply without CA

                        if ($linkState -eq 'enabled' -and $profileState -eq 'enabled') {
                            $baseLineProfileFound = $true
                            $enabledBaseLineProfiles += [PSCustomObject]@{
                                ProfileId          = $profileItem.id
                                ProfileName        = $profileItem.name
                                ProfileState       = $profileState
                                ProfilePriority    = $priority
                                TLSPolicyId        = $tlsId
                                TLSPolicyName      = $plink.policy.name
                                TLSPolicyLinkState = $linkState
                            }
                            break
                        }
                    } elseif ($null -ne $priority -and $priority -lt $BASELINE_PROFILE_PRIORITY) {
                        # Security Profile: must be applied via Conditional Access
                        # Validate CA policies reference this profile via sessionControls
                        $matchedCAPolicies = @()
                        if ($caProfileLookup.ContainsKey($profileItem.id)) {
                            $matchedCAPolicies = $caProfileLookup[$profileItem.id]
                        }

                        if ($matchedCAPolicies.Count -gt 0 -and $profileState -eq 'enabled' -and $linkState -eq 'enabled') {
                            $enabledSecurityProfiles += [PSCustomObject]@{
                                ProfileId          = $profileItem.id
                                ProfileName        = $profileItem.name
                                ProfileState       = $profileState
                                ProfilePriority    = $priority
                                TLSPolicyId        = $tlsId
                                TLSPolicyName      = $plink.policy.name
                                TLSPolicyLinkState = $linkState
                                MatchedCAPolicies  = $matchedCAPolicies
                                CAPolicyCount      = $matchedCAPolicies.Count
                                DefaultAction      = if ($null -ne $tlsPolicy.settings) {
                                    $tlsPolicy.settings.defaultAction
                                }
                                else {
                                    'unknown'
                                }
                            }
                        }
                    }
                }
            }
            if ($baseLineProfileFound) {
                break
            }
        }
    }

    #endregion Data Processing
    #region Assessment logic

    $testResultMarkdown = ''
    $passed = $false
    $mdInfo = ''

    if ($null -eq $tlsInspectionPolicies -or $tlsInspectionPolicies.Count -eq 0) {
        $testResultMarkdown = "❌ TLS Inspection Policy has not been properly configured. `n`n%TestResult%"
        $passed = $false
    }
    elseif ($enabledBaseLineProfiles.Count -gt 0) {
        $testResultMarkdown = "✅ TLS Inspection Policy is enabled and properly configured to inspect encrypted outbound traffic.`n`n%TestResult%"
        $passed = $true
    }
    elseif ($enabledSecurityProfiles.Count -gt 0) {
        $testResultMarkdown = "✅ TLS Inspection Policy is enabled and properly configured to inspect encrypted outbound traffic.`n`n%TestResult%"
        $passed = $true
    }
    else {
        $testResultMarkdown = "❌ TLS Inspection Policy has not been properly configured.`n`n%TestResult%"
        $passed = $false
    }

    #endregion Assessment logic

    #region Report Generation

    if ($enabledBaseLineProfiles.Count -gt 0) {

        $mdInfo += "`n## TLS Inspection Policies Linked to Baseline Profiles`n`n"
        $mdInfo += "| Linked Profile Name | Linked Profile Priority | Linked Policy Name | Policy Link State | Profile State |`n"
        $mdInfo += "| :--- | :--- | :--- | :--- | :--- |`n"
        foreach ($policy in $enabledBaseLineProfiles) {
            $baseLineProfilePortalLink = "https://entra.microsoft.com/#view/Microsoft_Azure_Network_Access/EditProfileMenuBlade.MenuView/~/basics/profileId/$(($policy.ProfileId))"
            $tlsPolicyPortalLink = "https://entra.microsoft.com/#view/Microsoft_Azure_Network_Access/EditTlsInspectionPolicyMenuBlade.MenuView/~/basics/policyId/$(($policy.TLSPolicyId))"
            $profileName = Get-SafeMarkdown -Text $policy.ProfileName
            $profilePriority = $policy.ProfilePriority
            $tlsPolicyName = Get-SafeMarkdown -Text $policy.TLSPolicyName
            $tlsPolicyLinkState = $policy.TLSPolicyLinkState
            $profileState = $policy.ProfileState
            $mdInfo += "| [$profileName]($baseLineProfilePortalLink) | $profilePriority | [$tlsPolicyName]($tlsPolicyPortalLink) | $tlsPolicyLinkState | $profileState |`n"
        }
    }

    if ($enabledSecurityProfiles.Count -gt 0) {
        $mdInfo += "`n## Security Profiles Linked to Conditional Access Policies`n`n"
        $mdInfo += "| Linked Profile Name | Linked Profile Priority | CA Policy Names | CA Policy State | Profile State | TLS Inspection Policy Name | Default Action |`n"
        $mdInfo += "| :--- | :--- | :--- | :--- | :--- | :--- | :--- |`n"
        foreach ($enabledProfile in $enabledSecurityProfiles) {
            $securityProfilePortalLink = "https://entra.microsoft.com/#view/Microsoft_Azure_Network_Access/EditProfileMenuBlade.MenuView/~/basics/profileId/$(($enabledProfile.ProfileId))"
            $tlsPolicyPortalLink = "https://entra.microsoft.com/#view/Microsoft_Azure_Network_Access/EditTlsInspectionPolicyMenuBlade.MenuView/~/basics/policyId/$(($enabledProfile.TLSPolicyId))"
            $profileName = Get-SafeMarkdown -Text $enabledProfile.ProfileName
            $profilePriority = $enabledProfile.ProfilePriority
            # Build CA policy links
            $caPolicyLinksMarkdown = @()
            $caPolicyStatesList = @()
            foreach ($caPolicy in $enabledProfile.MatchedCAPolicies) {
                $caPolicyPortalLink = "https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/PolicyBlade/policyId/$($caPolicy.Id)"
                $safeName = Get-SafeMarkdown -Text $caPolicy.DisplayName
                $caPolicyLinksMarkdown += "[$safeName]($caPolicyPortalLink)"
                $caPolicyStatesList += $caPolicy.State
            }
            $caPolicyNamesLinked = $caPolicyLinksMarkdown -join ', '
            $caPolicyStates = $caPolicyStatesList -join ', '
            $profileState = $enabledProfile.ProfileState
            $tlsPolicyName = Get-SafeMarkdown -Text $enabledProfile.TLSPolicyName
            $defaultAction = $enabledProfile.DefaultAction
            $mdInfo += "| [$profileName]($securityProfilePortalLink) | $profilePriority | $caPolicyNamesLinked | $caPolicyStates | $profileState | [$tlsPolicyName]($tlsPolicyPortalLink) | $defaultAction |`n"
        }
    }

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


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

    # Add test result details
    Add-ZtTestResultDetail @params
}