-
Notifications
You must be signed in to change notification settings - Fork 122
Network-25409: Global Secure Access Web content filtering controls internet access based on website categories #745
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 7 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
e03c9ee
added test file
ashwinikarke 7c34420
added Test file
ashwinikarke a730098
added Test file
ashwinikarke 94018b5
taken pull
ashwinikarke b4eb371
taken pull
ashwinikarke cdc6a56
updated test
ashwinikarke eca61d6
taken pull
ashwinikarke b340228
updated output table
ashwinikarke a7740b8
updated endpoint
ashwinikarke 77caabb
taken pull
ashwinikarke 0f3ba7a
updated to use helper function
ashwinikarke File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
|
||
| <!--- Results ---> | ||
| %TestResult% |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,309 @@ | ||
| <# | ||
| .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,action&`$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 (regardless of enabled/disabled state) | ||
| $linkedProfiles = @() | ||
|
|
||
| foreach ($filteringProfile in $filteringProfiles) { | ||
| # Get profile policies safely | ||
| $profilePolicies = @() | ||
| if ($null -ne $filteringProfile.policies) { | ||
| $profilePolicies = $filteringProfile.policies | ||
| } | ||
|
|
||
| foreach ($policyLink in $profilePolicies) { | ||
| $plinkType = $policyLink.'@odata.type' | ||
| $linkedPolicyId = $null | ||
|
|
||
| # Only process filteringPolicyLink entries | ||
| if ($plinkType -eq '#microsoft.graph.networkaccess.filteringPolicyLink' -and $null -ne $policyLink.policy) { | ||
| $linkedPolicyId = $policyLink.policy.id | ||
| } | ||
|
|
||
| if ($null -ne $linkedPolicyId -and $linkedPolicyId -eq $policyId) { | ||
| # Determine profile type based on priority | ||
| $priority = if ($null -ne $filteringProfile.priority) { | ||
| [int]$filteringProfile.priority | ||
| } | ||
| else { | ||
| $null | ||
| } | ||
|
|
||
| # Per spec: Only process Baseline Profile (priority = 65000) or Security Profile (priority < 65000) | ||
| if ($null -eq $priority) { | ||
| continue | ||
| } | ||
|
|
||
| $linkState = if ($null -ne $policyLink.state) { | ||
| $policyLink.state | ||
| } | ||
| else { | ||
| 'unknown' | ||
| } | ||
|
|
||
| if ($priority -eq $BASELINE_PROFILE_PRIORITY) { | ||
| # Baseline Profile: passes regardless of enabled state | ||
| $profileInfo = [PSCustomObject]@{ | ||
| ProfileId = $filteringProfile.id | ||
| ProfileName = $filteringProfile.name | ||
| ProfileType = 'Baseline Profile' | ||
| ProfileState = $filteringProfile.state | ||
| ProfilePriority = $priority | ||
| PolicyLinkState = $linkState | ||
| PassesCriteria = $true | ||
| CAPolicy = $null | ||
| WebCategoryRules = $webCategoryRules | ||
| } | ||
| $passed = $true | ||
| $linkedProfiles += $profileInfo | ||
| } | ||
| elseif ($priority -lt $BASELINE_PROFILE_PRIORITY) { | ||
| # Security Profile: check if linked to enabled CA policy | ||
| $linkedCAPolicies = $caPolicies | Where-Object { | ||
| $null -ne $_.sessionControls.globalSecureAccessFilteringProfile -and | ||
| $_.sessionControls.globalSecureAccessFilteringProfile.profileId -eq $filteringProfile.id -and | ||
| $_.sessionControls.globalSecureAccessFilteringProfile.isEnabled -eq $true | ||
| } | ||
|
|
||
| $profileInfo = [PSCustomObject]@{ | ||
| ProfileId = $filteringProfile.id | ||
| ProfileName = $filteringProfile.name | ||
| ProfileType = 'Security Profile' | ||
| ProfileState = $filteringProfile.state | ||
| ProfilePriority = $priority | ||
| PolicyLinkState = $linkState | ||
| PassesCriteria = $false | ||
| CAPolicy = $null | ||
| WebCategoryRules = $webCategoryRules | ||
| } | ||
|
|
||
| if ($linkedCAPolicies) { | ||
| # Check if at least one CA policy is enabled | ||
| $enabledCAPolicies = $linkedCAPolicies | Where-Object { $_.state -eq 'enabled' } | ||
| if ($enabledCAPolicies) { | ||
| $profileInfo.PassesCriteria = $true | ||
| $passed = $true | ||
| } | ||
| $profileInfo.CAPolicy = $linkedCAPolicies | ||
| } | ||
|
|
||
| $linkedProfiles += $profileInfo | ||
| } | ||
| # Profiles with priority > 65000 or other unknown values are skipped | ||
| } | ||
| } | ||
| } | ||
|
|
||
| # Add policy with its linked profiles to collection | ||
| if ($linkedProfiles.Count -gt 0) { | ||
| $policiesWithWebCategory += [PSCustomObject]@{ | ||
| PolicyId = $policyId | ||
| PolicyName = $policyName | ||
| PolicyAction = $policyDetails.action | ||
| LinkedProfiles = $linkedProfiles | ||
| WebCategoryRules = $webCategoryRules | ||
| } | ||
| } | ||
| } | ||
|
|
||
| # 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)" | ||
|
|
||
| $policyBladeLink = "https://entra.microsoft.com/#view/Microsoft_Azure_Network_Access/WebFilteringPolicy.ReactView" | ||
| $policyNameWithLink = "[$safePolicyName]($policyBladeLink)" | ||
|
|
||
| # Process each webCategory rule | ||
| foreach ($rule in $profileInfo.WebCategoryRules) { | ||
| $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 += "| Security profile name | CA policy 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 | ||
| CAPolicyName = $caPolicy.displayName | ||
| CAPolicyId = $caPolicy.id | ||
| CAPolicyState = $caPolicy.state | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| foreach ($item in $uniqueCALinks.Values | Sort-Object ProfileName, CAPolicyName) { | ||
| $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)" | ||
|
|
||
| # Show actual state with indicator | ||
| $caPolicyState = if ($item.CAPolicyState -eq 'enabled') { '✅ Enabled' } else { '❌ Disabled' } | ||
|
|
||
| $mdInfo += "| $safeProfileName | $caPolicyNameWithLink | $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 | ||
| } | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.