diff --git a/src/powershell/tests/Test-Assessment.25409.md b/src/powershell/tests/Test-Assessment.25409.md new file mode 100644 index 000000000..45a3d6629 --- /dev/null +++ b/src/powershell/tests/Test-Assessment.25409.md @@ -0,0 +1,10 @@ +Without web content filtering policies based on website categories, users can freely access potentially malicious or inappropriate websites regardless of their location. Threat actors often leverage compromised or malicious websites across various categories to distribute malware, launch phishing campaigns, or establish command and control channels. When users navigate to these sites without category-based filtering, their devices can become infected with malware that establishes persistence mechanisms. Threat actors can then use these compromised endpoints to move laterally within the network, escalate privileges, and exfiltrate sensitive organizational data. Additionally, without categorization-based controls, organizations lack visibility into user browsing patterns that could indicate compromised accounts or insider threats. Web content filtering provides defense in depth by blocking entire categories of risky websites at the network edge before traffic reaches user endpoints, preventing initial access and reducing the attack surface across all internet-connected devices whether on or off the corporate network. + +**Remediation action** + +- [How to configure Global Secure Access web content filtering](https://learn.microsoft.com/en-us/entra/global-secure-access/how-to-configure-web-content-filtering) - Guide to create and manage web content filtering policies +- [Configure security profiles](https://learn.microsoft.com/en-us/entra/global-secure-access/how-to-configure-web-content-filtering#create-a-security-profile) - Guide to create and manage security profiles that group filtering policies +- [Link security profiles to Conditional Access](https://learn.microsoft.com/en-us/entra/global-secure-access/how-to-configure-web-content-filtering#create-and-link-conditional-access-policy) - Instructions for delivering security profiles through Conditional Access session controls + + +%TestResult% diff --git a/src/powershell/tests/Test-Assessment.25409.ps1 b/src/powershell/tests/Test-Assessment.25409.ps1 new file mode 100644 index 000000000..ee823e3ec --- /dev/null +++ b/src/powershell/tests/Test-Assessment.25409.ps1 @@ -0,0 +1,236 @@ +<# +.SYNOPSIS + Validates that web content filtering policies based on website categories are configured in Global Secure Access. + +.DESCRIPTION + This test checks if web content filtering policies using website categories (webCategory ruleType) are configured + and applied either through the Baseline Profile or through security profiles linked to active Conditional Access policies. + +.NOTES + Test ID: 25409 + Category: Global Secure Access + Required API: networkAccess/filteringProfiles, networkAccess/filteringPolicies, conditionalAccess/policies (beta) +#> + +function Test-Assessment-25409 { + [ZtTest( + Category = 'Global Secure Access', + ImplementationCost = 'Medium', + MinimumLicense = ('Entra_Premium_Internet_Access'), + Pillar = 'Network', + RiskLevel = 'Medium', + SfiPillar = 'Protect networks', + TenantType = ('Workforce', 'External'), + TestId = 25409, + Title = 'Global Secure Access Web content filtering controls internet access based on website categories', + UserImpact = 'Medium' + )] + [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 by website categories' + Write-ZtProgress -Activity $activity -Status 'Querying Web Content Filtering policies' + + # Q1: Get all Web Content Filtering policies (excluding "All Websites") + try { + $allFilteringPolicies = Invoke-ZtGraphRequest -RelativeUri 'networkAccess/filteringPolicies' -ApiVersion beta -ErrorAction Stop + $wcfPolicies = $allFilteringPolicies | Where-Object { $_.name -ne 'All websites' } + } + catch { + Write-PSFMessage "Failed to retrieve filtering policies: $_" -Tag Test -Level Warning + $wcfPolicies = @() + } + + Write-ZtProgress -Activity $activity -Status 'Querying filtering profiles' + + # Q2: Get all filtering profiles with their policies and priority + try { + $filteringProfilesQueryParams = @{ + '$select' = 'id,name,description,state,version,priority' + '$expand' = 'policies($select=id,state;$expand=policy($select=id,name,version))' + } + $filteringProfiles = Invoke-ZtGraphRequest -RelativeUri 'networkAccess/filteringProfiles' -QueryParameters $filteringProfilesQueryParams -ApiVersion beta -ErrorAction Stop + } + catch { + Write-PSFMessage "Failed to retrieve filtering profiles: $_" -Tag Test -Level Warning + $filteringProfiles = @() + } + + Write-ZtProgress -Activity $activity -Status 'Querying Conditional Access policies' + + # Q3 prep: Get all Conditional Access policies with session controls + $caPolicies = Get-ZtConditionalAccessPolicy + #endregion Data Collection + + #region Assessment Logic + # Initialize test variables + $testResultMarkdown = '' + $passed = $false + $policiesWithWebCategory = @() + + # Check if any Web Content Filtering policies exist (excluding "All Websites") + if (-not $wcfPolicies -or $wcfPolicies.Count -eq 0) { + $testResultMarkdown = '❌ Web Content Filtering policy is not configured.' + $passed = $false + } + else { + # Per spec: Check if webCategory policies exist in Baseline Profile or Security Profiles with enabled CA + foreach ($wcfPolicy in $wcfPolicies) { + $policyId = $wcfPolicy.id + $policyName = $wcfPolicy.name + + # Get full policy details with rules to check for webCategory + $policyDetails = Invoke-ZtGraphRequest -RelativeUri "networkAccess/filteringPolicies/$policyId`?`$select=id,name,version&`$expand=policyRules" -ApiVersion beta + $webCategoryRules = @($policyDetails.policyRules) | Where-Object { $_.ruleType -eq 'webCategory' } + + # Skip if no webCategory rules + if (-not $webCategoryRules) { + continue + } + + # Find profiles that have this policy linked using shared helper function + $findParams = @{ + PolicyId = $policyId + FilteringProfiles = $filteringProfiles + CAPolicies = $caPolicies + BaselinePriority = $BASELINE_PROFILE_PRIORITY + PolicyLinkType = 'filteringPolicyLink' + PolicyRules = $webCategoryRules + } + $linkedProfiles = Find-ZtProfilesLinkedToPolicy @findParams + + # Check if any linked profile passes criteria + $profilePasses = $linkedProfiles | Where-Object { $_.PassesCriteria -eq $true } + if ($profilePasses) { + $passed = $true + } + + # Add policy with its linked profiles to collection + if ($linkedProfiles.Count -gt 0) { + $policiesWithWebCategory += [PSCustomObject]@{ + PolicyId = $policyId + PolicyName = $policyName + LinkedProfiles = $linkedProfiles + } + } + } + + # Determine status message based on pass/fail + if ($passed) { + $testResultMarkdown = "✅ Web content filtering with web category controls is configured and applied through either the Baseline Profile or a security profile linked to an active Conditional Access policy. `n`n%TestResult%" + } + else { + $testResultMarkdown = "❌ No policies using web category filtering were found in the Baseline Profile or in security profiles linked to active Conditional Access policies. `n`n%TestResult%" + } + } + #endregion Assessment Logic + + #region Report Generation + # Build detailed markdown information + $mdInfo = '' + + if ($policiesWithWebCategory.Count -gt 0) { + # Table 1: Filtering Policies with Web Category Rules + $mdInfo += "`n## Filtering Policies with Web Category Rules`n`n" + $mdInfo += "| Profile type | Profile name | Policy name | Rule name | Web categories | State |`n" + $mdInfo += "| :--- | :--- | :--- | :--- | :--- | :--- |`n" + + foreach ($wcfPolicy in $policiesWithWebCategory | Sort-Object -Property PolicyName) { + $safePolicyName = Get-SafeMarkdown $wcfPolicy.PolicyName + + foreach ($profileInfo in $wcfPolicy.LinkedProfiles) { + $safeProfileName = Get-SafeMarkdown $profileInfo.ProfileName + $policyLinkState = $profileInfo.PolicyLinkState + + # Create blade links + $profileBladeLink = "https://entra.microsoft.com/#view/Microsoft_Azure_Network_Access/EditProfileMenuBlade.MenuView/~/basics/profileId/$($profileInfo.ProfileId)" + $profileNameWithLink = "[$safeProfileName]($profileBladeLink)" + + $encodedPolicyName = [System.Uri]::EscapeDataString($wcfPolicy.PolicyName) + $policyBladeLink = "https://entra.microsoft.com/#view/Microsoft_Azure_Network_Access/EditFilteringPolicyMenuBlade.MenuView/~/Basics/policyId/$($wcfPolicy.PolicyId)/title/$encodedPolicyName/defaultMenuItemId/Basics" + $policyNameWithLink = "[$safePolicyName]($policyBladeLink)" + + # Process each webCategory rule + foreach ($rule in $profileInfo.PolicyRules) { + $safeRuleName = Get-SafeMarkdown $rule.name + $webCategories = ($rule.destinations | ForEach-Object { $_.displayName }) -join ', ' + $safeWebCategories = Get-SafeMarkdown $webCategories + + # Show state with indicator + $stateDisplay = if ($policyLinkState -eq 'enabled') { '✅ Enabled' } else { '❌ Disabled' } + + $mdInfo += "| $($profileInfo.ProfileType) | $profileNameWithLink | $policyNameWithLink | $safeRuleName | $safeWebCategories | $stateDisplay |`n" + } + } + } + + # Table 2: Conditional Access Linkages (for Security Profiles only) + $securityProfiles = $policiesWithWebCategory.LinkedProfiles | Where-Object { $_.ProfileType -eq 'Security Profile' -and $null -ne $_.CAPolicy } + if ($securityProfiles.Count -gt 0) { + $mdInfo += "`n## Conditional Access Linkages (for Security Profiles only)`n`n" + $mdInfo += "| CA policy name | Security profile name | CA policy state |`n" + $mdInfo += "| :--- | :--- | :--- |`n" + + # Build unique CA linkages + $uniqueCALinks = @{} + foreach ($policy in $policiesWithWebCategory) { + foreach ($profileInfo in $policy.LinkedProfiles) { + if ($profileInfo.ProfileType -eq 'Security Profile' -and $null -ne $profileInfo.CAPolicy -and $profileInfo.CAPolicy.Count -gt 0) { + foreach ($caPolicy in $profileInfo.CAPolicy) { + $key = "$($profileInfo.ProfileId)|$($caPolicy.id)" + if (-not $uniqueCALinks.ContainsKey($key)) { + $uniqueCALinks[$key] = [PSCustomObject]@{ + ProfileName = $profileInfo.ProfileName + ProfileId = $profileInfo.ProfileId + CAPolicyName = $caPolicy.displayName + CAPolicyId = $caPolicy.id + CAPolicyState = $caPolicy.state + } + } + } + } + } + } + + foreach ($item in $uniqueCALinks.Values | Sort-Object CAPolicyName, ProfileName) { + $safeProfileName = Get-SafeMarkdown $item.ProfileName + $safeCAPolicyName = Get-SafeMarkdown $item.CAPolicyName + + $caPolicyPortalLink = "https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/PolicyBlade/policyId/$($item.CAPolicyId)" + $caPolicyNameWithLink = "[$safeCAPolicyName]($caPolicyPortalLink)" + + $profilePortalLink = "https://entra.microsoft.com/#view/Microsoft_Azure_Network_Access/EditProfileMenuBlade.MenuView/~/basics/profileId/$($item.ProfileId)" + $profileNameWithLink = "[$safeProfileName]($profilePortalLink)" + + # Show actual state with indicator + $caPolicyState = if ($item.CAPolicyState -eq 'enabled') { '✅ Enabled' } else { '❌ Disabled' } + + $mdInfo += "| $caPolicyNameWithLink | $profileNameWithLink | $caPolicyState |`n" + } + } + + # Add portal links at the end + $mdInfo += "`n### Portal links`n`n" + $mdInfo += "- [Web content filtering policies](https://entra.microsoft.com/#view/Microsoft_Azure_Network_Access/WebFilteringPolicy.ReactView)`n" + $mdInfo += "- [Security profiles](https://entra.microsoft.com/#view/Microsoft_Azure_Network_Access/FilteringPolicyProfiles.ReactView)`n" + } + + # Replace the placeholder with detailed information + $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo + #endregion Report Generation + + $params = @{ + TestId = '25409' + Title = 'Web content filtering with website categories is configured' + Status = $passed + Result = $testResultMarkdown + } + + # Add test result details + Add-ZtTestResultDetail @params +}