tests/Test-Assessment.25410.ps1

<#
.SYNOPSIS
    Validates that web content filtering policies are configured and enforced in Global Secure Access.
 
.DESCRIPTION
    This test checks if web content filtering policies exist and are properly linked to security profiles
    that are either assigned to Conditional Access policies or configured in the Baseline Profile which
    applies to all internet traffic. Web content filtering provides protection against malicious websites,
    phishing sites, and inappropriate content categories at the network edge.
 
.NOTES
    Test ID: 25410
    Category: Global Secure Access
    Pillar: Network
    Required API: networkAccess/filteringPolicies (beta), networkAccess/filteringProfiles (beta)
#>


function Test-Assessment-25410 {
    [ZtTest(
        Category = 'Global Secure Access',
        ImplementationCost = 'Medium',
        MinimumLicense = ('Entra_Premium_Internet_Access'),
        Pillar = 'Network',
        RiskLevel = 'Medium',
        SfiPillar = 'Protect networks',
        TenantType = ('Workforce', 'External'),
        TestId = 25410,
        Title = 'Internet traffic is protected by web content filtering policies in Global Secure Access',
        UserImpact = 'Low'
    )]
    [CmdletBinding()]
    param()

    # Define constants
    [int]$BASELINE_PROFILE_PRIORITY = 65000

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

    $activity = 'Checking Global Secure Access web content filtering'
    Write-ZtProgress -Activity $activity -Status 'Getting filtering policies'

    # Query Q1: List all web content filtering policies
    $filteringPolicies = $null
    $errorMsg = $null
    try {
        $filteringPolicies = Invoke-ZtGraphRequest `
            -RelativeUri 'networkAccess/filteringPolicies?$expand=policyRules' `
            -ApiVersion beta `
            -ErrorAction Stop
    }
    catch {
        $errorMsg = $_
        Write-PSFMessage "Failed to get filtering policies: $_" -Tag Test -Level Warning
    }

    Write-ZtProgress -Activity $activity -Status 'Getting security profiles'

    # Query Q2: List all security profiles with linked policies and CA policies
    $securityProfiles = $null
    try {
        $securityProfiles = Invoke-ZtGraphRequest `
            -RelativeUri 'networkAccess/filteringProfiles?$expand=policies($expand=policy),conditionalAccessPolicies' `
            -ApiVersion beta `
            -ErrorAction Stop
    }
    catch {
        if (-not $errorMsg) {
            $errorMsg = $_
        }
        Write-PSFMessage "Failed to get security profiles: $_" -Tag Test -Level Warning
    }

    # Extract values
    $policies = if ($filteringPolicies) { $filteringPolicies } else { @() }
    $profiles = if ($securityProfiles) { $securityProfiles } else { @() }

    # Collect all filtering policy IDs for use in assessment and reporting
    $q1PolicyIds = @($policies | ForEach-Object { $_.id })
    #endregion Data Collection

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

    # Check if API calls failed
    if ($errorMsg) {
        # Investigate: Cannot query API
        $passed = $false
        $customStatus = 'Investigate'
        $testResultMarkdown = "⚠️ Unable to determine web content filtering status due to API connection failure or insufficient permissions.`n`n%TestResult%"
    }
    # Check if both policies and profiles exist
    elseif ($policies.Count -gt 0 -and $profiles.Count -gt 0) {
        # Find Baseline Profile (priority = 65000)
        $baselineProfile = $profiles | Where-Object { $_.priority -eq $BASELINE_PROFILE_PRIORITY }

        # Check Condition B: Baseline Profile has filtering policy linked
        $baselineHasWCF = $false
        if ($baselineProfile) {
            $filteringPolicyLinks = $baselineProfile.policies | Where-Object {
                $_.'@odata.type' -eq '#microsoft.graph.networkaccess.filteringPolicyLink' -and
                $_.policy.id -in $q1PolicyIds
            }
            $baselineHasWCF = ($filteringPolicyLinks.Count -gt 0)
        }

        # Check Condition A: Non-Baseline profiles with filtering policy AND CA policy
        $nonBaselineProfilesWithWCFandCA = $profiles | Where-Object {
            $_.priority -ne $BASELINE_PROFILE_PRIORITY -and
            ($_.policies | Where-Object {
                $_.'@odata.type' -eq '#microsoft.graph.networkaccess.filteringPolicyLink' -and
                $_.policy.id -in $q1PolicyIds
            }).Count -gt 0 -and
            $_.conditionalAccessPolicies.Count -gt 0
        }

        # Determine pass/fail
        if ($baselineHasWCF -or $nonBaselineProfilesWithWCFandCA.Count -gt 0) {
            $passed = $true
            $testResultMarkdown = "✅ Web content filtering policies are configured and enforced - either through security profiles assigned to Conditional Access policies or through the Baseline Profile which applies to all internet traffic.`n`n%TestResult%"
        }
    }

    # Default failure message (if not API error and not passed)
    if (-not $errorMsg -and -not $passed) {
        $testResultMarkdown = "❌ Web content filtering is not properly configured - either no policies exist, policies are not linked to security profiles, or security profiles with filtering policies are not enforced (no CA policy assignment and not using Baseline Profile).`n`n%TestResult%"
    }
    #endregion Assessment Logic

    #region Report Generation
    $mdInfo = ''

    # Table 1: Web Content Filtering Policies
    if ($policies.Count -gt 0) {
        $table1Title = 'Web Content Filtering Policies'
        $table1Link = 'https://entra.microsoft.com/#view/Microsoft_Azure_Network_Access/WebFilteringPolicy.ReactView'

        $table1Template = @'
 
## [{0}]({1})
 
| Policy Name | Action | Rules Count | Last Modified |
| :---------- | :----- | ----------: | :------------ |
{2}
'@


        $table1Rows = ''
        foreach ($policy in $policies) {
            $policyName = $policy.name
            $policyId = $policy.id
            $action = $policy.action
            $rulesCount = if ($policy.policyRules) { $policy.policyRules.Count } else { 0 }
            $lastModified = if ($policy.lastModifiedDateTime) {
                ([DateTime]$policy.lastModifiedDateTime).ToString('yyyy-MM-dd HH:mm')
            } else { 'N/A' }

            # Create policy blade link
            $safePolicyName = Get-SafeMarkdown $policyName
            $encodedPolicyName = [System.Uri]::EscapeDataString($policyName)
            $policyBladeLink = "https://entra.microsoft.com/#view/Microsoft_Azure_Network_Access/EditFilteringPolicyMenuBlade.MenuView/~/Basics/policyId/$policyId/title/$encodedPolicyName/defaultMenuItemId/Basics"
            $policyNameWithLink = "[$safePolicyName]($policyBladeLink)"

            $table1Rows += "| $policyNameWithLink | $action | $rulesCount | $lastModified |`n"
        }

        $mdInfo += $table1Template -f $table1Title, $table1Link, $table1Rows
    }

    # Table 2: Security Profiles with Linked Policies
    if ($profiles.Count -gt 0) {
        $table2Title = 'Security Profiles with Linked Policies'
        $table2Link = 'https://entra.microsoft.com/#view/Microsoft_Azure_Network_Access/FilteringPolicyProfiles.ReactView'

        $table2Template = @'
 
## [{0}]({1})
 
| Profile Name | State | Priority | Filtering Policies Linked | CA Policies Assigned | Is Baseline |
| :----------- | :---- | -------: | :----------------------- | -------------------: | :---------- |
{2}
'@


        $table2Rows = ''
        foreach ($securityProfile in $profiles) {
            $profileName = $securityProfile.name
            $profileId = $securityProfile.id
            $state = $securityProfile.state
            $priority = $securityProfile.priority
            $isBaseline = if ($priority -eq $BASELINE_PROFILE_PRIORITY) { 'Yes' } else { 'No' }

            # Create profile blade link
            $safeProfileName = Get-SafeMarkdown $profileName
            $profileBladeLink = "https://entra.microsoft.com/#view/Microsoft_Azure_Network_Access/EditProfileMenuBlade.MenuView/~/basics/profileId/$profileId"
            $profileNameWithLink = "[$safeProfileName]($profileBladeLink)"

            # Get filtering policy links
            $filteringPolicyLinks = $securityProfile.policies | Where-Object {
                $_.'@odata.type' -eq '#microsoft.graph.networkaccess.filteringPolicyLink' -and
                $_.policy.id -in $q1PolicyIds
            }

            $linkedPolicyNames = if ($filteringPolicyLinks.Count -gt 0) {
                ($filteringPolicyLinks | ForEach-Object { Get-SafeMarkdown $_.policy.name }) -join ', '
            } else {
                'None'
            }

            $caCount = if ($isBaseline -eq 'Yes') {
                'N/A'
            } else {
                $securityProfile.conditionalAccessPolicies.Count
            }

            $table2Rows += "| $profileNameWithLink | $state | $priority | $linkedPolicyNames | $caCount | $isBaseline |`n"
        }

        $mdInfo += $table2Template -f $table2Title, $table2Link, $table2Rows
    }

    # Table 3: Conditional Access Policies Assigned to Security Profiles
    $caPolicies = @()
    foreach ($securityProfile in $profiles) {
        if ($securityProfile.conditionalAccessPolicies -and $securityProfile.conditionalAccessPolicies.Count -gt 0) {
            foreach ($caPolicy in $securityProfile.conditionalAccessPolicies) {
                $caPolicies += [PSCustomObject]@{
                    CAPolicyName = $caPolicy.displayName
                    ProfileName  = $securityProfile.name
                    CAPolicyId   = $caPolicy.id
                }
            }
        }
    }

    if ($caPolicies.Count -gt 0) {
        $table3Title = 'Conditional Access Policies Assigned to Security Profiles'
        $table3Link = 'https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/Policies'

        $table3Template = @'
 
## [{0}]({1})
 
| CA Policy Name | Security Profile |
| :------------- | :--------------- |
{2}
'@


        $table3Rows = ''
        foreach ($caPolicy in $caPolicies) {
            $safeCAPolicyName = Get-SafeMarkdown $caPolicy.CAPolicyName
            $safeProfileName = Get-SafeMarkdown $caPolicy.ProfileName
            $table3Rows += "| [$safeCAPolicyName](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/PolicyBlade/policyId/$($caPolicy.CAPolicyId)) | $safeProfileName |`n"
        }

        $mdInfo += $table3Template -f $table3Title, $table3Link, $table3Rows
    }

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

    $params = @{
        TestId = '25410'
        Title  = 'Internet traffic is protected by web content filtering policies in Global Secure Access'
        Status = $passed
        Result = $testResultMarkdown
    }
    if ($customStatus) {
        $params.CustomStatus = $customStatus
    }
    Add-ZtTestResultDetail @params
}