tests/Test-Assessment.27000.ps1

<#
.SYNOPSIS
    Checks if high-risk web content filtering categories (Criminal activity, Hacking, Illegal software) are blocked.
 
.DESCRIPTION
    This check evaluates whether Global Secure Access web content filtering policies block three
    high-risk Liability categories: Criminal activity, Hacking, and Illegal software. It validates
    that the blocking is effective through either the baseline profile or security profiles with
    active Conditional Access enforcement.
 
.NOTES
    Test ID: 27000
    Category: Global Secure Access
    Risk Level: High
#>


function Test-Assessment-27000 {
    [ZtTest(
        Category = 'Global Secure Access',
        ImplementationCost = 'Low',
        MinimumLicense = ('Entra_Premium_Internet_Access'),
        Pillar = 'Network',
        RiskLevel = 'High',
        SfiPillar = 'Protect networks',
        TenantType = ('Workforce'),
        TestId = 27000,
        Title = 'Web content filtering blocks high-risk categories',
        UserImpact = 'Low'
    )]
    [CmdletBinding()]
    param()

    #region Helper Functions
    function New-FailedCategoryResults {
        <#
        .SYNOPSIS
            Creates failed category results when policies or profiles are missing.
        #>

        param(
            [Parameter(Mandatory)]
            [array]$RequiredCategories,

            [Parameter(Mandatory)]
            [hashtable]$CategoryDisplayNames
        )

        $results = @()
        foreach ($catName in $RequiredCategories) {
            $results += [PSCustomObject]@{
                Category   = $CategoryDisplayNames[$catName]
                EnforcedBy = 'None'
                CAEnforced = 'N/A'
                Status     = 'Not Blocked'
            }
        }
        return $results
    }

    function Get-CategoryBlockStatus {
        <#
        .SYNOPSIS
            Evaluates whether a specific WCF category is blocked through an effective profile.
 
        .DESCRIPTION
            Finds policies covering the category, identifies linked profiles, and determines
            the effective profile based on priority and CA enforcement criteria.
        #>

        param(
            [Parameter(Mandatory)]
            [string]$CategoryName,

            [Parameter(Mandatory)]
            [string]$CategoryDisplayName,

            [Parameter(Mandatory)]
            [array]$FilteringPolicies,

            [Parameter(Mandatory)]
            [array]$FilteringProfiles,

            [Parameter(Mandatory)]
            [AllowNull()]
            [array]$CAPolicies,

            [Parameter(Mandatory)]
            [int]$BaselinePriority
        )

        # Find all policies that cover this category
        $policiesCoveringCategory = @($FilteringPolicies | Where-Object {
            $policy = $_
            $webCatRules = @($policy.policyRules | Where-Object { $_.ruleType -eq 'webCategory' })
            $webCatRules | Where-Object {
                $_.destinations | Where-Object { $_.name -eq $CategoryName }
            }
        })

        # Collect profile candidates from all matching policies
        $profileCandidates = @()
        foreach ($policy in $policiesCoveringCategory) {
            $findParams = @{
                PolicyId          = $policy.id
                FilteringProfiles = $FilteringProfiles
                CAPolicies        = $CAPolicies
                BaselinePriority  = $BaselinePriority
                PolicyLinkType    = 'filteringPolicyLink'
                PolicyRules       = @($policy.policyRules)
            }
            $linkedProfiles = Find-ZtProfilesLinkedToPolicy @findParams

            foreach ($linkedProfile in $linkedProfiles) {
                # Skip disabled profiles
                if ($linkedProfile.ProfileState -ne 'enabled') {
                    Write-PSFMessage "Skipping disabled profile '$($linkedProfile.ProfileName)'" -Level Verbose
                    continue
                }

                # Get the profile object to access policies collection
                $filteringProfile = $FilteringProfiles | Where-Object { $_.id -eq $linkedProfile.ProfileId }
                if (-not $filteringProfile) {
                    Write-PSFMessage "Profile '$($linkedProfile.ProfileName)' not found in filteringProfiles collection" -Level Warning
                    continue
                }

                # Find the policy link to get priority and action
                foreach ($policyLink in $filteringProfile.policies) {
                    if ($policyLink.policy.id -ne $policy.id) { continue }

                    # Skip disabled policy links
                    if ($policyLink.state -ne 'enabled') {
                        Write-PSFMessage "Skipping disabled policy link in profile '$($linkedProfile.ProfileName)' for policy '$($policy.name)'" -Level Verbose
                        continue
                    }

                    $linkPriority = try { [int]$policyLink.priority } catch { [int]::MaxValue }

                    # Use policy action directly (not overridden at profile level)
                $linkAction = if ($policyLink.policy.action) {
                                            $policyLink.policy.action.ToString().ToLower()
                                        }
                    else {
                        Write-PSFMessage "Policy action is null for policy '$($policy.name)' - defaulting to 'unknown'" -Level Warning
                        'unknown'
                    }

                    $profileCandidates += [PSCustomObject]@{
                        ProfileId      = $linkedProfile.ProfileId
                        ProfileName    = $linkedProfile.ProfileName
                        ProfilePriority= $linkedProfile.ProfilePriority
                        IsBaseline     = ($linkedProfile.ProfileType -eq 'Baseline Profile')
                        PolicyAction   = $linkAction
                        PolicyPriority = $linkPriority
                        PassesCriteria = $linkedProfile.PassesCriteria
                    }
                }
            }
        }

        # Sort by profile priority, then policy priority
        $profileCandidates = @($profileCandidates | Sort-Object ProfilePriority, PolicyPriority)

        # Find effective profile per spec logic
        $effectiveProfileName = 'None'
        $caEnforced = 'N/A'
        $status = 'Not blocked'

        foreach ($pc in $profileCandidates) {
            if ($pc.IsBaseline) {
                # Baseline profile is always effective
                $effectiveProfileName = $pc.ProfileName
                $caEnforced = 'N/A'
                $status = if ($pc.PolicyAction -eq 'block') { 'Blocked' } else { 'Not blocked' }
                break
            }
            else {
                # Security profile - check if it passes CA enforcement criteria
                if ($pc.PassesCriteria) {
                    $effectiveProfileName = $pc.ProfileName
                    $caEnforced = 'Yes'
                    $status = if ($pc.PolicyAction -eq 'block') { 'Blocked' } else { 'Not blocked' }
                    break
                }
            }
        }

        return [PSCustomObject]@{
            Category   = $CategoryDisplayName
            EnforcedBy = $effectiveProfileName
            CAEnforced = $caEnforced
            Status     = $status
        }
    }
    #endregion Helper Functions

    #region Data Collection
    Write-PSFMessage '🟦 Start high-risk WCF category block evaluation' -Tag Test -Level VeryVerbose
    $activity = 'Checking high-risk WCF categories are blocked'

    $filteringPolicies = $null
    $filteringProfiles = $null
    $caPolicies = $null
    $errorMsg = $null

    try {
        # Q1: Get all filtering policies with policy rules
        Write-ZtProgress -Activity $activity -Status 'Getting filtering policies'
        $filteringPolicies = Invoke-ZtGraphRequest -RelativeUri 'networkAccess/filteringPolicies' -QueryParameters @{ '$expand' = 'policyRules' } -ApiVersion beta -ErrorAction Stop
        Write-PSFMessage "Found $($filteringPolicies.Count) filtering policies" -Level Verbose

        # Q2: Get all filtering profiles with expanded policies (following 25407 pattern)
        if ($filteringPolicies -and $filteringPolicies.Count -gt 0) {
            Write-ZtProgress -Activity $activity -Status 'Getting filtering profiles'
            $filteringProfiles = Invoke-ZtGraphRequest -RelativeUri 'networkAccess/filteringProfiles' -QueryParameters @{ '$expand' = 'policies($expand=policy)' } -ApiVersion beta -ErrorAction Stop
            Write-PSFMessage "Found $($filteringProfiles.Count) filtering profiles" -Level Verbose
        }

        # Q3: Get CA policies (following 25407 pattern)
        if ($filteringPolicies -and $filteringPolicies.Count -gt 0 -and
            $filteringProfiles -and $filteringProfiles.Count -gt 0) {
            Write-ZtProgress -Activity $activity -Status 'Getting Conditional Access policies'
            $caPolicies = Get-ZtConditionalAccessPolicy
        }
    }
    catch {
        $errorMsg = $_
        Write-PSFMessage "Failed to retrieve data: $_" -Level Error
    }
    #endregion Data Collection

    #region Assessment Logic
    # Required categories to check
    $requiredCategories = @('CriminalActivity', 'Hacking', 'IllegalSoftware')
    $categoryDisplayNames = @{
        'CriminalActivity' = 'Criminal activity'
        'Hacking'          = 'Hacking'
        'IllegalSoftware'  = 'Illegal software'
    }

    # Early validation: Check if we have the required data to proceed
    $passed = $false
    $categoryResults = @()

    if($errorMsg) {
        # Error occurred during data collection, cannot proceed with assessment -> Fail
        Write-PSFMessage "Error during data collection: $errorMsg" -Level Error
        $testResultMarkdown = "❌ Failed to retrieve necessary data for assessment.`n`nError: $errorMsg"
    }
    elseif(-not $filteringPolicies -or $filteringPolicies.Count -eq 0 ){
        Write-PSFMessage "No WCF policies found -> Fail" -Level Warning
        $categoryResults = New-FailedCategoryResults -RequiredCategories $requiredCategories -CategoryDisplayNames $categoryDisplayNames
        $blockedCount = 0
        $notBlockedCount = $requiredCategories.Count
    }
    elseif ($filteringPolicies -and $filteringPolicies.count -eq 1 -and $filteringPolicies[0].name -eq 'All Websites'){
        Write-PSFMessage "Only default 'All Websites' policy exists -> Fail" -Level Warning
        $categoryResults = New-FailedCategoryResults -RequiredCategories $requiredCategories -CategoryDisplayNames $categoryDisplayNames
        $blockedCount = 0
        $notBlockedCount = $requiredCategories.Count
    }
    elseif (-not $filteringProfiles -or $filteringProfiles.Count -eq 0) {
        Write-PSFMessage "No filtering profiles found -> Fail" -Level Warning
        $categoryResults = New-FailedCategoryResults -RequiredCategories $requiredCategories -CategoryDisplayNames $categoryDisplayNames
        $blockedCount = 0
        $notBlockedCount = $requiredCategories.Count
    }
    else {
        [int]$BASELINE_PROFILE_PRIORITY = 65000

        # Evaluate each category using the helper function
        foreach ($catName in $requiredCategories) {
            $catDisplay = $categoryDisplayNames[$catName]

            $getCategoryParams = @{
                CategoryName        = $catName
                CategoryDisplayName = $catDisplay
                FilteringPolicies   = $filteringPolicies
                FilteringProfiles   = $filteringProfiles
                CAPolicies          = $caPolicies
                BaselinePriority    = $BASELINE_PROFILE_PRIORITY
            }

            $categoryResult = Get-CategoryBlockStatus @getCategoryParams
            $categoryResults += $categoryResult
        }

        # Determine pass/fail
        $blockedCount = @($categoryResults | Where-Object { $_.Status -eq 'Blocked' }).Count
        $notBlockedCount = $requiredCategories.Count - $blockedCount
        $passed = $blockedCount -eq $requiredCategories.Count
    }


    #endregion Assessment Logic

    #region Report Generation
    # Only generate full report if we have category results
    if ($errorMsg) {
        # Error message already set, no table needed
    }
    else {
        if ($passed) {
            $testResultMarkdown = "✅ High-risk web content filtering categories (Criminal activity, Hacking, Illegal software) are blocked across enabled security profiles, protecting users from liability risks and malicious content.`n`n%TestResult%"
        }
        else {
            $testResultMarkdown = "❌ One or more high-risk web content filtering categories (Criminal activity, Hacking, Illegal software) are not blocked. Configure web content filtering policies to block these Liability categories to protect against security risks and policy violations.`n`n%TestResult%"
        }

        $reportTitle = 'Web Content Filtering – Category block status'
        $portalLink = 'https://entra.microsoft.com/#view/Microsoft_Azure_Network_Access/WebFilteringPolicy.ReactView'

        # Build table with exactly 4 columns as per spec
        $tableRows = ''
        foreach ($row in $categoryResults) {
            $categoryName = Get-SafeMarkdown -Text $row.Category
            $enforcedBy = Get-SafeMarkdown -Text $row.EnforcedBy
            $statusIcon = if ($row.Status -eq 'Blocked') { '✅ Blocked' } else { '❌ Not blocked' }

            $tableRows += "| $categoryName | $enforcedBy | $($row.CAEnforced) | $statusIcon |`n"
        }

        $formatTemplate = @'
 
## [{0}]({1})
 
| Category | Enforced by | CA enforced | Status |
| :------- | :---------- | :---------- | :----- |
{2}
 
**Summary:**
- Total required categories: {3}
- Categories blocked: {4}
- Categories not blocked: {5}
'@


        $mdInfo = $formatTemplate -f $reportTitle, $portalLink, $tableRows, $requiredCategories.Count, $blockedCount, $notBlockedCount
        $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo
    }
    #endregion Report Generation

    $params = @{
        TestId = '27000'
        Title  = 'Web content filtering blocks high-risk categories'
        Status = $passed
        Result = $testResultMarkdown
    }

    Add-ZtTestResultDetail @params
}