From cc5e48a35c386904c09d7bb433d011ccfb0a7bd3 Mon Sep 17 00:00:00 2001 From: Sandeep Jha Date: Mon, 5 Jan 2026 09:30:27 +0530 Subject: [PATCH 01/49] initial commit --- src/powershell/tests/Test-Assessment.25408.md | 10 + .../tests/Test-Assessment.25408.ps1 | 228 ++++++++++++++++++ 2 files changed, 238 insertions(+) create mode 100644 src/powershell/tests/Test-Assessment.25408.md create mode 100644 src/powershell/tests/Test-Assessment.25408.ps1 diff --git a/src/powershell/tests/Test-Assessment.25408.md b/src/powershell/tests/Test-Assessment.25408.md new file mode 100644 index 000000000..2db8c5815 --- /dev/null +++ b/src/powershell/tests/Test-Assessment.25408.md @@ -0,0 +1,10 @@ +Web Content Filtering in Microsoft Entra Internet Access helps organizations control access to websites based on its web categories, domains or url, reducing exposure to malicious or inappropriate content. When traffic is routed through Microsoft Entra Internet Access, filtering policies can block or allow entire categories like Gambling or Social Media, or specific domains/url, ensuring safer browsing across all devices and locations. + +Configuring these policies is critical for security and compliance. It prevents phishing and malware risks, enforces corporate standards, and improves productivity by restricting non-business sites. Combined with identity-aware Conditional Access, Web Content Filtering delivers dynamic, cloud-based protection that aligns with modern Zero Trust principles. + +**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) + + +%TestResult% diff --git a/src/powershell/tests/Test-Assessment.25408.ps1 b/src/powershell/tests/Test-Assessment.25408.ps1 new file mode 100644 index 000000000..e44148f6d --- /dev/null +++ b/src/powershell/tests/Test-Assessment.25408.ps1 @@ -0,0 +1,228 @@ +<# +.SYNOPSIS + Checks that Global Secure Access web content filtering is enabled and configured +.DESCRIPTION + Verifies that Web Content Filtering policies are configured and applied either through the Baseline Profile + or through Security Profiles linked to active Conditional Access policies. This ensures that organizations + control access to websites based on categories, domains, or URLs to reduce exposure to malicious or + inappropriate content. + +.NOTES + Test ID: 25408 + Category: Global Secure Access + Required API: networkAccess/filteringPolicies, networkAccess/filteringProfiles, conditionalAccess/policies +#> + +function Test-Assessment-25408 { + [ZtTest( + Category = 'Global Secure Access', + ImplementationCost = 'Medium', + MinimumLicense = ('Entra_Premium_Internet_Access'), + Pillar = 'Network', + RiskLevel = 'Medium', + SfiPillar = 'Protect networks', + TenantType = ('Workforce','External'), + TestId = '25408', + Title = 'Global Secure Access web content filtering is enabled and configured', + UserImpact = 'Medium' + )] + [CmdletBinding()] + param() + + #region Data Collection + Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose + $activity = 'Checking Global Secure Access web content filtering configuration' + Write-ZtProgress -Activity $activity -Status 'Querying Web Content Filtering policies' + + # Q1: Get all Web Content Filtering policies (excluding "All Websites") + $allFilteringPolicies = Invoke-ZtGraphRequest -RelativeUri 'networkAccess/filteringPolicies' -ApiVersion beta + $wcfPolicies = $allFilteringPolicies | Where-Object { $_.name -ne 'All websites' } + + Write-ZtProgress -Activity $activity -Status 'Querying filtering profiles' + + # Q2: Get all filtering profiles with their policies and priority + $securitySettings = Invoke-ZtGraphRequest -RelativeUri "networkAccess/filteringProfiles?`$select=id,name,description,state,version,priority&`$expand=policies(`$select=id,state;`$expand=policy(`$select=id,name,version))" -ApiVersion beta + + 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 + $customStatus = $null + $appliedPolicies = @() + + # Check if any Web Content Filtering policies exist (excluding "All Websites") + if (-not $wcfPolicies -or $wcfPolicies.Count -eq 0) { + $testResultMarkdown = '❌ No Web Content Filtering policies are configured in the tenant. Organizations should configure web content filtering to control access to websites based on categories, domains, or URLs.' + $passed = $false + } + else { + # Check if WCF policies are linked to profiles + foreach ($wcfPolicy in $wcfPolicies) { + $policyId = $wcfPolicy.id + $policyName = $wcfPolicy.name + $policyAction = $wcfPolicy.action + + # Find profiles that have this policy linked + $linkedSettings = @() + + foreach ($securityItem in $securitySettings) { + # Check if this profile contains the WCF policy + $policyLink = $securityItem.policies | Where-Object { + $_.'@odata.type' -eq '#microsoft.graph.networkaccess.filteringPolicyLink' -and + $_.policy.id -eq $policyId -and + $_.state -eq 'enabled' + } + + if ($policyLink) { + # Determine profile type based on priority + $itemType = if ($securityItem.priority -eq 65000) { 'Baseline Profile' } else { 'Security Profile' } + + $itemInfo = [PSCustomObject]@{ + ProfileId = $securityItem.id + ProfileName = $securityItem.name + ProfileType = $itemType + ProfileState = $securityItem.state + ProfilePriority = $securityItem.priority + PolicyLinkState = $policyLink.state + IsApplied = $false + CAPolicy = $null + } + + # If Baseline Profile and enabled, it's automatically applied + if ($itemType -eq 'Baseline Profile' -and $securityItem.state -eq 'enabled') { + $itemInfo.IsApplied = $true + } + # If Security Profile, check if it's linked to an active CA policy + elseif ($itemType -eq 'Security Profile' -and $securityItem.state -eq 'enabled') { + # Step 4: Check for Conditional Access policy linkage + $linkedCAPolicies = $caPolicies | Where-Object { + $_.state -eq 'enabled' -and + $null -ne $_.sessionControls.globalSecureAccessFilteringProfile -and + $_.sessionControls.globalSecureAccessFilteringProfile.profileId -eq $securityItem.id -and + $_.sessionControls.globalSecureAccessFilteringProfile.isEnabled -eq $true + } + + if ($linkedCAPolicies) { + $itemInfo.IsApplied = $true + $itemInfo.CAPolicy = $linkedCAPolicies + } + } + + $linkedSettings += $itemInfo + } + } + + # If this policy is applied through at least one profile, add it to applied policies + if ($linkedSettings | Where-Object { $_.IsApplied -eq $true }) { + $appliedPolicies += [PSCustomObject]@{ + PolicyId = $policyId + PolicyName = $policyName + PolicyAction = $policyAction + LinkedSettings = $linkedSettings + } + } + } + + # Determine pass/fail + if ($appliedPolicies.Count -gt 0) { + $passed = $true + $testResultMarkdown = "✅ Web Content Filtering policies are enabled and configured. `n`n%TestResult%" + } + else { + $passed = $false + $testResultMarkdown = "❌ Web Content Filtering policies exist but are not applied to any users through Baseline Profile or Security Profiles linked to Conditional Access policies. `n`n%TestResult%" + } + } + #endregion Assessment Logic + + #region Report Generation + # Build detailed markdown information + $portalLink = 'https://entra.microsoft.com/#view/Microsoft_AAD_IAM/CompanyNetworkAccessMenuBlade/~/GlobalSecureAccessWebContentFiltering' + $mdInfo = '' + + if ($wcfPolicies -and $wcfPolicies.Count -gt 0) { + # Show all WCF policies and their application status + $mdInfo += "## [Web Content Filtering Policies]($portalLink)`n`n" + + if ($appliedPolicies.Count -gt 0) { + foreach ($appliedPolicy in $appliedPolicies) { + $safePolicyName = Get-SafeMarkdown $appliedPolicy.PolicyName + $mdInfo += "### ✅ $safePolicyName`n`n" + $mdInfo += "**Action**: $($appliedPolicy.PolicyAction)`n`n" + + # Show profiles this policy is linked to + $appliedItems = $appliedPolicy.LinkedSettings | Where-Object { $_.IsApplied -eq $true } + + foreach ($itemInfo in $appliedItems) { + $safeName = Get-SafeMarkdown $itemInfo.ProfileName + $itemBladeLink = "https://entra.microsoft.com/#view/Microsoft_AAD_IAM/CompanyNetworkAccessMenuBlade/~/GlobalSecureAccessInternetAccessProfiles" + + $mdInfo += "#### [$safeName]($itemBladeLink) ($($itemInfo.ProfileType))`n`n" + $mdInfo += "| Property | Value |`n" + $mdInfo += "|----------|-------|`n" + $mdInfo += "| Profile Id | ``$($itemInfo.ProfileId)`` |`n" + $mdInfo += "| Priority | $($itemInfo.ProfilePriority) |`n" + $mdInfo += "| State | $($itemInfo.ProfileState) |`n" + $mdInfo += "| Policy link state | $($itemInfo.PolicyLinkState) |`n" + + # If linked via CA policy, show CA policy details + if ($itemInfo.CAPolicy) { + $mdInfo += "`n**Applied through Conditional Access Policies:**`n`n" + $mdInfo += "| Policy name | State | Id |`n" + $mdInfo += "|-------------|-------|-----|`n" + + foreach ($caPolicy in $itemInfo.CAPolicy) { + $safeCAPolicyName = Get-SafeMarkdown $caPolicy.displayName + $caPolicyLink = "https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/PolicyBlade/policyId/$($caPolicy.id)" + $mdInfo += "| [$safeCAPolicyName]($caPolicyLink) | $($caPolicy.state) | ``$($caPolicy.id)`` |`n" + } + } + else { + $mdInfo += "`n*Applied through Baseline Profile (automatically applies to all users)*`n" + } + + $mdInfo += "`n" + } + } + } + + # Show unapplied policies if any + $unappliedPolicies = $wcfPolicies | Where-Object { $_.id -notin $appliedPolicies.PolicyId } + if ($unappliedPolicies.Count -gt 0) { + $mdInfo += "### ❌ Policies Not Applied to Users`n`n" + $mdInfo += "The following Web Content Filtering policies exist but are not applied through any profile:`n`n" + $mdInfo += "| Policy name | Action | Id |`n" + $mdInfo += "|-------------|--------|-----|`n" + + foreach ($unappliedPolicy in $unappliedPolicies) { + $safeName = Get-SafeMarkdown $unappliedPolicy.name + $mdInfo += "| $safeName | $($unappliedPolicy.action) | ``$($unappliedPolicy.id)`` |`n" + } + } + } + + # Replace the placeholder with detailed information + $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo + #endregion Report Generation + + $params = @{ + TestId = '25408' + Title = 'Global Secure Access web content filtering is enabled and configured' + Status = $passed + Result = $testResultMarkdown + } + + # Add CustomStatus if needed + if ($null -ne $customStatus) { + $params.CustomStatus = $customStatus + } + + # Add test result details + Add-ZtTestResultDetail @params +} From c94eab2e6d282887ea6507ba1b8942030d625887 Mon Sep 17 00:00:00 2001 From: Afif Ahmed Patel Date: Tue, 6 Jan 2026 12:47:19 +0530 Subject: [PATCH 02/49] Adding test for assessment 25395 --- src/powershell/tests/Test-Assessment.25395.md | 23 ++ .../tests/Test-Assessment.25395.ps1 | 365 ++++++++++++++++++ 2 files changed, 388 insertions(+) create mode 100644 src/powershell/tests/Test-Assessment.25395.md create mode 100644 src/powershell/tests/Test-Assessment.25395.ps1 diff --git a/src/powershell/tests/Test-Assessment.25395.md b/src/powershell/tests/Test-Assessment.25395.md new file mode 100644 index 000000000..0749e8589 --- /dev/null +++ b/src/powershell/tests/Test-Assessment.25395.md @@ -0,0 +1,23 @@ +When organizations configure Microsoft Entra Private Access with broad application segments—such as wide IP ranges, multiple protocols, or Quick Access configurations—they effectively replicate the over-permissive access model of traditional VPNs. This approach contradicts the Zero Trust principle of least-privilege access, where users should only reach the specific resources required for their role. Threat actors who compromise a user's credentials or device can leverage these broad network permissions to perform reconnaissance, identifying additional systems and services within the permitted range. + + +With visibility into the network topology, they can escalate privileges by targeting vulnerable systems, move laterally to access sensitive data stores or administrative interfaces, and establish persistence by deploying backdoors across multiple accessible systems. The lack of granular segmentation also complicates incident response, as security teams cannot quickly determine which specific resources a compromised identity could access. By contrast, per-application segmentation with tightly scoped destination hosts, specific ports, and Custom Security Attributes enables dynamic, attribute-driven Conditional Access enforcement—requiring stronger authentication or device compliance for high-risk applications while streamlining access to lower-risk resources. + + +This approach aligns with the Zero Trust "verify explicitly" principle by ensuring each access request is evaluated against the specific security requirements of the target application rather than applying uniform policies to broad network segments. + + +**Remediation action** +- [Transition from Quick Access](https://learn.microsoft.com/en-us/entra/global-secure-access/how-to-configure-per-app-access) to per-app Private Access by creating individual Global Secure Access enterprise applications with specific FQDNs, IP addresses, and ports for each private resource. +- [Use Application Discovery](https://learn.microsoft.com/en-us/entra/global-secure-access/how-to-application-discovery) to identify which resources users access through Quick Access, then create targeted Private Access apps for those resources. +- [Create Custom Security Attribute sets](https://learn.microsoft.com/en-us/entra/fundamentals/custom-security-attributes-add) and definitions to categorize Private Access applications by risk level, department, or compliance requirements. +- [Assign Custom Security Attributes](https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/custom-security-attributes-apps) to Private Access application service principals to enable attribute-based access control. +- [Create Conditional Access policies using application filters](https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-filter-for-applications) to target Private Access apps based on their Custom Security Attributes, enforcing granular controls like MFA or device compliance. +- [Apply Conditional Access policies to Private Access](https://learn.microsoft.com/en-us/entra/global-secure-access/how-to-target-resource-private-access-apps) apps from within Global Secure Access for streamlined configuration. + +Review +- [Zero Trust network segmentation guidance for software-defined perimeters](https://learn.microsoft.com/en-us/security/zero-trust/deploy/networks#1-network-segmentation-and-software-defined-perimeters). + + + +%TestResult% diff --git a/src/powershell/tests/Test-Assessment.25395.ps1 b/src/powershell/tests/Test-Assessment.25395.ps1 new file mode 100644 index 000000000..141b44607 --- /dev/null +++ b/src/powershell/tests/Test-Assessment.25395.ps1 @@ -0,0 +1,365 @@ +<# +.SYNOPSIS + Validates that Entra Private Access applications enforce least-privilege + using granular network segments and Custom Security Attributes (CSA). + +.DESCRIPTION + This test evaluates Private Access applications to ensure segmentation + follows least-privilege principles and supports attribute-based + Conditional Access targeting. + +.NOTES + Test ID: 25395 + Category: Global Secure Access + Required APIs: applications (beta), servicePrincipals (beta), conditionalAccess/policies (beta) +#> + +function Test-Assessment-25395 { + + [ZtTest( + Category = 'Global Secure Access', + ImplementationCost = 'High', + MinimumLicense = 'Entra_Premium_Private_Access', + Pillar = 'Network', + RiskLevel = 'High', + SfiPillar = 'Protect networks', + TenantType = 'Workforce', + TestId = 25395, + Title = 'Private Access application segments enforce least-privilege access', + UserImpact = 'Medium' + )] + [CmdletBinding()] + param() + + #region Helper Functions + + function Test-IsBroadCidr { + <# + .SYNOPSIS + Checks if a CIDR range is overly permissive (/16 or broader). + .OUTPUTS + System.Boolean - True if CIDR is /16 or broader, false otherwise. + #> + param([string]$Cidr) + if ($Cidr -match '/(\d+)$') { return ([int]$matches[1] -le 16) } + return $false + } + + function Test-IsBroadIpRange { + <# + .SYNOPSIS + Checks if an IP range spans more than 256 addresses. + .OUTPUTS + System.Boolean - True if range exceeds 256 addresses, false otherwise. + #> + param([string]$Range) + if ($Range -match '^([\d\.]+)-([\d\.]+)$') { + $start = [System.Net.IPAddress]::Parse($matches[1]).GetAddressBytes() + $end = [System.Net.IPAddress]::Parse($matches[2]).GetAddressBytes() + [array]::Reverse($start) + [array]::Reverse($end) + return (([BitConverter]::ToUInt32($end,0) - [BitConverter]::ToUInt32($start,0)) -gt 256) + } + return $false + } + + function Test-IsBroadPortRange { + <# + .SYNOPSIS + Checks if a port range is overly broad (>10 ports or fully open). + .OUTPUTS + System.Boolean - True if port range is considered too broad, false otherwise. + #> + param([string]$Port) + if ($Port -eq '1-65535') { return $true } + if ($Port -match '^(\d+)-(\d+)$' -and (([int]$matches[2] - [int]$matches[1]) -gt 10)) { return $true } + return $false + } + + function Test-IsAdRpcException { + <# + .SYNOPSIS + Checks if a port range is a valid Active Directory RPC ephemeral port exception. + .OUTPUTS + System.Boolean - True if port is a valid AD RPC exception, false otherwise. + #> + param([string]$AppName, [string]$Port) + if ($AppName -match 'Active Directory|Domain Controller|AD DS') { + if ($Port -in @('49152-65535','1025-5000')) { return $true } + } + return $false + } + + function Test-IsAdWellKnownPort { + <# + .SYNOPSIS + Checks if a port is a well-known Active Directory port. + .OUTPUTS + System.Boolean - True if port is a valid AD well-known port, false otherwise. + #> + param([string]$Port) + $valid = @('53','88','135','389','445','464','636','3268','3269') + if ($Port -match '^(\d+)-(\d+)$') { + return ($matches[1] -eq $matches[2] -and $valid -contains $matches[1]) + } + return ($valid -contains $Port) + } + + #endregion Helper Functions + + #region Data Collection + + Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose + $activity = 'Evaluating Private Access application segmentation' + Write-ZtProgress -Activity $activity -Status 'Querying applications' + + # Query Q1: List all Private Access enterprise applications + $apps = Invoke-ZtGraphRequest -RelativeUri "applications?`$filter=(tags/any(t:t eq 'PrivateAccessNonWebApplication') or tags/any(t:t eq 'NetworkAccessQuickAccessApplication'))&`$select=id,displayName,appId,tags" -ApiVersion beta + + # Query Q2: Retrieve service principals with Custom Security Attributes + $servicePrincipals = Invoke-ZtGraphRequest -RelativeUri "servicePrincipals?`$filter=(tags/any(t:t eq 'PrivateAccessNonWebApplication') or tags/any(t:t eq 'NetworkAccessQuickAccessApplication'))&`$select=id,appId,displayName,customSecurityAttributes&`$count=true" -ApiVersion beta -ConsistencyLevel eventual + + # Query Q3: Retrieve enabled Conditional Access policies + $caPolicies = $null + $filterPolicies = @() + + if ($null -ne $apps -and $apps.Count -gt 0) { + + Write-ZtProgress -Activity $activity -Status 'Checking Conditional Access policies' + + $allCAPolicies = Get-ZtConditionalAccessPolicy + $caPolicies = $allCAPolicies | Where-Object { $_.state -eq 'enabled' } + + if ($caPolicies) { + $filterPolicies = $caPolicies | Where-Object { + $_.conditions.applications.applicationFilter + } + } + } + + + # Initialize evaluation containers + $passed = $false + $customStatus = $null + $testResultMarkdown = '' + $broadAccessApps = @() + $appsWithoutCSA = @() + $segmentFindings = @() + $appResults = @() + + #endregion Data Collection + + #region Assessment Logic + + # Step 1: Check if any per-app Private Access applications exist + if ($apps) { + + Write-ZtProgress -Activity $activity -Status 'Evaluating application segments' + + foreach ($app in $apps) { + + # Query Q4: Retrieve application segments for the current app + $segments = Invoke-ZtGraphRequest -RelativeUri "applications/$($app.id)/onPremisesPublishing/segmentsConfiguration/microsoft.graph.ipSegmentConfiguration/applicationSegments" -ApiVersion beta + + $hasBroadSegment = $false + $hasWildcardDns = $false + $hasBroadPorts = $false + $segmentSummary = @() + + if (-not $segments -or $segments.Count -eq 0) { + $segmentSummary = @('No segments configured') + } + + foreach ($segment in $segments) { + + # Step 2: Evaluate segment destination granularity + $issues = @() + + $segmentSummary += "$($segment.destinationType):$($segment.destinationHost):$($segment.ports -join ',')" + + switch ($segment.destinationType) { + 'dnsSuffix' { + $hasWildcardDns = $true + $issues += 'Wildcard DNS' + } + 'ipRangeCidr' { + if (Test-IsBroadCidr $segment.destinationHost) { + $hasBroadSegment = $true + $issues += 'Broad CIDR' + } + } + 'ipRange' { + if (Test-IsBroadIpRange $segment.destinationHost) { + $hasBroadSegment = $true + $issues += 'Broad IP range' + } + } + } + + # Step 3: Evaluate port breadth with AD RPC exceptions + foreach ($port in $segment.ports) { + if (Test-IsBroadPortRange $port) { + if (-not (Test-IsAdRpcException -AppName $app.displayName -Port $port) ` + -and -not (Test-IsAdWellKnownPort $port)) { + $hasBroadPorts = $true + $issues += 'Broad port range' + } + } + } + + # Step 4: Flag dual-protocol usage combined with broad scope + if ($segment.protocol -eq 'tcp,udp' -and $issues.Count -gt 0) { + $hasBroadPorts = $true + $issues += 'Dual protocol with broad scope' + } + + if ($issues.Count -gt 0) { + $segmentFindings += [PSCustomObject]@{ + AppName = $app.displayName + SegmentId = $segment.id + Issue = ($issues -join ', ') + Destination = $segment.destinationHost + Ports = ($segment.ports -join ', ') + } + } + } + + # Step 5: Identify apps with overly broad access + if ($hasBroadSegment -or $hasWildcardDns -or $hasBroadPorts) { + $broadAccessApps += $app + } + + # Step 6: Check CSA presence for the app + $sp = $servicePrincipals | Where-Object { $_.appId -eq $app.appId } + if (-not $sp.customSecurityAttributes) { + $appsWithoutCSA += $app + } + + # Determine per-app status including Manual Review when filterPolicies exist + $appStatus = if (-not $sp.customSecurityAttributes) { + 'Fail – Missing CSA' + } elseif ($hasBroadSegment -or $hasWildcardDns -or $hasBroadPorts) { + 'Fail – Broad segment' + } elseif ($filterPolicies.Count -gt 0) { + 'Manual Review' + } else { + 'Pass' + } + + $appResults += [PSCustomObject]@{ + AppName = $app.displayName + AppObjectId = $app.id + AppId = $app.appId + SegmentType = if ($segments) { ($segments.destinationType | Select-Object -Unique) -join ', ' } else { 'None' } + SegmentScope = ($segmentSummary -join ' | ') + HasCSA = [bool]$sp.customSecurityAttributes + Status = $appStatus + } + + + } + } + + # Step 7: Determine overall test result (Pass / Fail / Investigate) + + if (-not $apps -or $apps.Count -eq 0) { + + $customStatus = 'Investigate' + $testResultMarkdown = + "⚠️ No per-app Private Access applications configured. Please review the documentation on how to configure Private Access applications with granular network segments.`n`n%TestResult%" + + } + elseif ($broadAccessApps.Count -eq 0 -and $appsWithoutCSA.Count -eq 0) { + + if ($filterPolicies.Count -gt 0) { + + # Pass conditions met but filterPolicies exist - requires manual review + $customStatus = 'Investigate' + $testResultMarkdown = + "⚠️ Private Access applications exist with appropriate segmentation and CSAs assigned. CA policies use applicationFilter targeting. Manual review required to verify CA policy coverage for these apps.`n`n%TestResult%" + + } + else { + + $passed = $true + $testResultMarkdown = + "✅ All Private Access applications are configured with granular network segments and are protected by Conditional Access policies using Custom Security Attributes, enforcing least-privilege access.`n`n%TestResult%" + + } + + } + else { + + $passed = $false + $testResultMarkdown = + "❌ One or more Private Access applications have overly broad network segments or lack Custom Security Attribute-based Conditional Access policies, potentially allowing excessive network access.`n`n%TestResult%" + + } + + #endregion Assessment Logic + + #region Report Generation + + $mdInfo = "`n## Summary`n`n" + $mdInfo += "| Metric | Value |`n|---|---|`n" + $mdInfo += "| Total Private Access apps | $($apps.Count) |`n" + $mdInfo += "| Apps with broad segments | $($broadAccessApps.Count) |`n" + $mdInfo += "| Apps with CSA assigned | $($apps.Count - $appsWithoutCSA.Count) |`n" + $mdInfo += "| Apps without CSA | $($appsWithoutCSA.Count) |`n" + $mdInfo += "| CA policies using applicationFilter | $($filterPolicies.Count) |`n`n" + + if ($appResults.Count -gt 0) { + $tableRows = "" + $formatTemplate = @' +## [Application details](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/EnterpriseApplicationListBladeV3/fromNav/globalSecureAccess/applicationType/GlobalSecureAccessApplication) + +| App name | Segment type | Segment scope | Has CSAs | Status | +|---|---|---|---|---| +{0} + +'@ + foreach ($r in $appResults) { + $appLink = "https://entra.microsoft.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/overview/appId/$($r.AppId)" + $linkedAppName = "[{0}]({1})" -f (Get-SafeMarkdown $r.AppName), $appLink + $hasCSAText = if ($r.HasCSA) {'Yes'} else {'No'} + $tableRows += "| $linkedAppName | $($r.SegmentType) | $($r.SegmentScope) | $hasCSAText | $($r.Status) |`n" + } + $mdInfo += $formatTemplate -f $tableRows + } + + + if ($segmentFindings.Count -gt 0) { + $tableRows = "" + $formatTemplate = @' +## Segment Findings + +| App name | Segment id | Issue | Destination | Ports | Recommendation | +|---|---|---|---|---|---| +{0} + +'@ + foreach ($f in $segmentFindings) { + $tableRows += "| $(Get-SafeMarkdown $f.AppName) | $($f.SegmentId) | $($f.Issue) | $($f.Destination) | $($f.Ports) | Narrow destination and ports |`n" + } + $mdInfo += $formatTemplate -f $tableRows + } + + $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo + + $params = @{ + TestId = '25395' + Title = 'Private Access application segments enforce least-privilege access' + Status = $passed + Result = $testResultMarkdown + } + + # Add CustomStatus if status is 'Investigate' + if ($null -ne $customStatus) { + $params.CustomStatus = $customStatus + } + + # Add test result details + Add-ZtTestResultDetail @params + + #endregion Report Generation +} From c7c73985472579f8656ba67d7515a8d4c16928e0 Mon Sep 17 00:00:00 2001 From: Manoj K Date: Tue, 6 Jan 2026 13:47:41 +0530 Subject: [PATCH 03/49] Feature-35009 --- src/powershell/tests/Test-Assessment.35009.md | 15 ++++ .../tests/Test-Assessment.35009.ps1 | 85 +++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 src/powershell/tests/Test-Assessment.35009.md create mode 100644 src/powershell/tests/Test-Assessment.35009.ps1 diff --git a/src/powershell/tests/Test-Assessment.35009.md b/src/powershell/tests/Test-Assessment.35009.md new file mode 100644 index 000000000..985f5c781 --- /dev/null +++ b/src/powershell/tests/Test-Assessment.35009.md @@ -0,0 +1,15 @@ +Co-authoring allows multiple users to simultaneously edit Office documents stored in SharePoint and OneDrive. When sensitivity labels apply encryption to documents, co-authoring capabilities are disabled by default, forcing users to work sequentially rather than collaboratively. Without co-authoring enabled for encrypted files, users face productivity barriers that incentivize removing encryption or working with unprotected copies to maintain collaboration velocity. The EnableLabelCoauth tenant-wide setting allows co-authoring on encrypted documents while maintaining protection and access controls defined by sensitivity labels. + +**Remediation action** + +To enable co-authoring for encrypted documents: + +1. Connect to Security & Compliance PowerShell: `Connect-IPPSSession` +2. Run the command: `Set-PolicyConfig -EnableLabelCoauth $true` +3. Wait for replication (changes may take up to 24 hours to propagate fully) +4. Users may need to sign out and sign back in to Office applications + +- [Enable co-authoring for encrypted documents](https://learn.microsoft.com/en-us/purview/sensitivity-labels-coauthoring) +- [Set-PolicyConfig cmdlet reference](https://learn.microsoft.com/en-us/powershell/module/exchange/set-policyconfig) + +%TestResult% diff --git a/src/powershell/tests/Test-Assessment.35009.ps1 b/src/powershell/tests/Test-Assessment.35009.ps1 new file mode 100644 index 000000000..b77e6fedb --- /dev/null +++ b/src/powershell/tests/Test-Assessment.35009.ps1 @@ -0,0 +1,85 @@ +<# +.SYNOPSIS + +#> + +function Test-Assessment-35009 { + [ZtTest( + Category = 'Sensitivity Labels', + ImplementationCost = 'Low', + MinimumLicense = ('Microsoft 365 E5'), + Pillar = 'Data', + RiskLevel = 'Low', + SfiPillar = '', + TenantType = ('Workforce'), + TestId = 35009, + Title = 'Co-Authoring Enabled for Encrypted Documents', + UserImpact = 'High' + )] + [CmdletBinding()] + param() + + #region Data Collection + Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose + + $activity = "Checking co-authoring is enabled for encrypted documents" + Write-ZtProgress -Activity $activity -Status "Getting policy configuration" + + # Q1: Retrieve policy configuration settings + try { + $policyConfig = Get-PolicyConfig -ErrorAction Stop + } + catch { + Write-PSFMessage "Failed to retrieve policy configuration: $_" -Tag Test -Level Warning + Add-ZtTestResultDetail -SkippedBecause NotConnectedExchangeOnline + return + } + + # Q2: Check EnableLabelCoauth property value + $enableLabelCoauth = $policyConfig.EnableLabelCoauth + + #endregion Data Collection + + #region Assessment Logic + + if ($enableLabelCoauth -eq $true) { + $passed = $true + $testResultMarkdown = "✅ Co-authoring is enabled for encrypted documents with sensitivity labels.`n`n%TestResult%" + } + elseif ($enableLabelCoauth -eq $false) { + $passed = $false + $testResultMarkdown = "❌ Co-authoring is disabled for encrypted documents.`n`n%TestResult%" + } + else { + $passed = $false + $customStatus = 'Investigate' + $testResultMarkdown = "⚠️ Policy configuration exists but EnableLabelCoauth setting cannot be determined.`n`n%TestResult%" + } + + #endregion Assessment Logic + + #region Report Generation + $reportDetails = "" + $reportDetails += "`n`n## Configuration Details`n`n" + $reportDetails += "| Setting | Status |`n" + $reportDetails += "| :------ | :----- |`n" + $statusDisplay = if ($enableLabelCoauth -eq $true) { '✅ Enabled' } elseif ($enableLabelCoauth -eq $false) { '❌ Disabled' } else { '-' } + $reportDetails += "| EnableLabelCoauth | $statusDisplay |`n" + + $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $reportDetails + + #endregion Report Generation + + $params = @{ + TestId = '35009' + Title = 'Co-Authoring Enabled for Encrypted Documents' + Status = $passed + Result = $testResultMarkdown + } + if ($customStatus) { + $params.CustomStatus = $customStatus + } + + Add-ZtTestResultDetail @params + +} From 56b763719f4a21e037c9d56801b913a0498ae80f Mon Sep 17 00:00:00 2001 From: aahmed-spec Date: Tue, 6 Jan 2026 14:27:02 +0530 Subject: [PATCH 04/49] Updated region markers and app conditional check Refactor assessment script by updating region markers and improving condition checks. --- src/powershell/tests/Test-Assessment.25395.ps1 | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25395.ps1 b/src/powershell/tests/Test-Assessment.25395.ps1 index 141b44607..53fc74db8 100644 --- a/src/powershell/tests/Test-Assessment.25395.ps1 +++ b/src/powershell/tests/Test-Assessment.25395.ps1 @@ -137,6 +137,9 @@ function Test-Assessment-25395 { } } + #endregion Data Collection + + #region Assessment Logic # Initialize evaluation containers $passed = $false @@ -146,13 +149,9 @@ function Test-Assessment-25395 { $appsWithoutCSA = @() $segmentFindings = @() $appResults = @() - - #endregion Data Collection - - #region Assessment Logic - + # Step 1: Check if any per-app Private Access applications exist - if ($apps) { + if ($null -ne $apps -and $apps.Count -gt 0) { Write-ZtProgress -Activity $activity -Status 'Evaluating application segments' @@ -344,8 +343,10 @@ function Test-Assessment-25395 { $mdInfo += $formatTemplate -f $tableRows } + # Replace the placeholder with detailed information $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo - + #endregion Report Generation + $params = @{ TestId = '25395' Title = 'Private Access application segments enforce least-privilege access' @@ -360,6 +361,4 @@ function Test-Assessment-25395 { # Add test result details Add-ZtTestResultDetail @params - - #endregion Report Generation } From 4e0014ef4ee3844eca38002778aed4352e3bbd2c Mon Sep 17 00:00:00 2001 From: Ashwini Karke Date: Tue, 6 Jan 2026 14:27:25 +0530 Subject: [PATCH 05/49] added test file --- src/powershell/tests/Test-Assessment.35013.md | 28 ++++ .../tests/Test-Assessment.35013.ps1 | 153 ++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 src/powershell/tests/Test-Assessment.35013.md create mode 100644 src/powershell/tests/Test-Assessment.35013.ps1 diff --git a/src/powershell/tests/Test-Assessment.35013.md b/src/powershell/tests/Test-Assessment.35013.md new file mode 100644 index 000000000..cacf61e61 --- /dev/null +++ b/src/powershell/tests/Test-Assessment.35013.md @@ -0,0 +1,28 @@ +# Encryption-Enabled Sensitivity Labels + +## Description + +Sensitivity labels provide classification capabilities, but without encryption, labels merely mark content as sensitive without preventing unauthorized access. Encryption-enabled labels apply Azure Rights Management protection to documents and emails, enforcing access controls that persist with the content regardless of where it is stored or shared. + +Users with "Confidential" labels on documents can still forward those files to unauthorized recipients unless encryption prevents file access based on identity. Organizations investing in sensitivity label frameworks without enabling encryption gain visibility into data classification but lack technical enforcement of protection policies. + +Encrypted labels ensure that only authorized users and applications can decrypt content, preventing data exfiltration even if files are leaked, stolen, or improperly shared. At least one encryption-enabled label should exist for high-value data requiring protection beyond classification metadata. + +## How to fix + +To enable encryption on sensitivity labels: + +1. Navigate to **Microsoft Purview portal** → **Information protection** → **Labels** +2. Create a new label or edit an existing label +3. Under label scope, ensure **Items** is selected +4. In protection settings, enable **Apply or remove encryption** +5. Configure encryption settings: + - **Assign permissions now** (define specific users/groups) + - **Let users assign permissions** (Do Not Forward or user-defined) +6. Save and publish the label through a label policy + +## Learn more + +- [Restrict access to content by using encryption in sensitivity labels](https://learn.microsoft.com/en-us/microsoft-365/compliance/encryption-sensitivity-labels) +- [Apply encryption using sensitivity labels](https://learn.microsoft.com/en-us/purview/sensitivity-labels-office-apps) +- [Information protection in Microsoft 365](https://learn.microsoft.com/en-us/microsoft-365/compliance/information-protection) diff --git a/src/powershell/tests/Test-Assessment.35013.ps1 b/src/powershell/tests/Test-Assessment.35013.ps1 new file mode 100644 index 000000000..e045ab3fa --- /dev/null +++ b/src/powershell/tests/Test-Assessment.35013.ps1 @@ -0,0 +1,153 @@ +<# +.SYNOPSIS + Validates that at least one encryption-enabled sensitivity label is configured. + +.DESCRIPTION + This test checks if at least one sensitivity label has encryption enabled. Encryption-enabled labels + apply Azure Rights Management protection to documents and emails, enforcing access controls that + persist with the content regardless of where it is stored or shared. + +.NOTES + Test ID: 35013 + Category: Data + Pillar: Data + Required Module: ExchangeOnlineManagement + Required Connection: Exchange Online (Security & Compliance Center) +#> + +function Test-Assessment-35013 { + [ZtTest( + Category = 'Data', + ImplementationCost = 'Medium', + MinimumLicense = 'Microsoft_365_E3', + Pillar = 'Data', + RiskLevel = 'High', + SfiPillar = 'Protect tenants and production systems', + TenantType = ('Workforce', 'External'), + TestId = 35013, + Title = 'Encryption-enabled sensitivity labels are configured', + UserImpact = 'High' + )] + [CmdletBinding()] + param() + + #region Data Collection + Write-PSFMessage 'Start Encryption-Enabled Labels evaluation' -Tag Test -Level VeryVerbose + + $activity = 'Checking Encryption-Enabled Labels' + Write-ZtProgress -Activity $activity -Status 'Getting sensitivity labels' + + # Q1: Retrieve all sensitivity labels + try { + $labels = Get-Label -ErrorAction Stop + } + catch { + Write-PSFMessage "Failed to retrieve sensitivity labels: $_" -Tag Test -Level Warning + + $params = @{ + TestId = '35013' + Title = 'Encryption-enabled sensitivity labels are configured' + Status = $false + Result = "❌ Unable to retrieve sensitivity labels. Ensure you are connected to Exchange Online Security & Compliance Center.`n`nError: $_" + } + Add-ZtTestResultDetail @params + return + } + + # Q2: Filter for encryption-enabled labels + $encryptionEnabledLabels = $labels | Where-Object { $_.EncryptionEnabled -eq $true } + + # Q3: Check if any labels have null/undefined EncryptionEnabled property + $undeterminedLabels = $labels | Where-Object { $null -eq $_.EncryptionEnabled } + #endregion Data Collection + + #region Assessment Logic + $passed = $false + $customStatus = $null + + # Investigate: Cannot determine encryption configuration + if ($undeterminedLabels.Count -gt 0 -and $encryptionEnabledLabels.Count -eq 0) { + $passed = $true + $customStatus = 'Investigate' + } + # Pass: At least one encryption-enabled label exists + elseif ($encryptionEnabledLabels.Count -ge 1) { + $passed = $true + } + # Fail: No encryption-enabled labels exist + else { + $passed = $false + } + #endregion Assessment Logic + + #region Report Generation + $testResultMarkdown = '' + + if ($customStatus -eq 'Investigate') { + $testResultMarkdown = "⚠️ Labels exist but encryption configuration cannot be determined`n`n" + } + elseif ($passed) { + $testResultMarkdown = "✅ At least one encryption-enabled sensitivity label is configured`n`n" + } + else { + $testResultMarkdown = "❌ No encryption-enabled labels exist; all labels provide classification only`n`n" + } + + # Build detailed information + $mdInfo = "## Summary`n`n" + $mdInfo += "- **Total Sensitivity Labels**: $($labels.Count)`n" + $mdInfo += "- **Encryption-Enabled Labels**: $($encryptionEnabledLabels.Count)`n" + + if ($undeterminedLabels.Count -gt 0) { + $mdInfo += "- **Labels with Undetermined Encryption**: $($undeterminedLabels.Count)`n" + } + + $mdInfo += "`n" + + if ($encryptionEnabledLabels.Count -gt 0) { + $mdInfo += "## Encryption-Enabled Labels`n`n" + $mdInfo += "| Label name | Enabled | Content type |`n" + $mdInfo += "| :--------- | :------ | :----------- |`n" + + foreach ($label in ($encryptionEnabledLabels | Sort-Object DisplayName)) { + $enabledStatus = if ($label.Disabled -eq $false) { '✅ Yes' } else { '❌ No' } + $contentType = if ($label.ContentType) { $label.ContentType -join ', ' } else { 'All' } + + $mdInfo += "| $(Get-SafeMarkdown $label.DisplayName) | $enabledStatus | $contentType |`n" + } + $mdInfo += "`n" + } + + if ($undeterminedLabels.Count -gt 0) { + $mdInfo += "## Labels with Undetermined Encryption Configuration`n`n" + $mdInfo += "| Label name | Enabled | Content type |`n" + $mdInfo += "| :--------- | :------ | :----------- |`n" + + foreach ($label in ($undeterminedLabels | Sort-Object DisplayName)) { + $enabledStatus = if ($label.Disabled -eq $false) { '✅ Yes' } else { '❌ No' } + $contentType = if ($label.ContentType) { $label.ContentType -join ', ' } else { 'All' } + + $mdInfo += "| $(Get-SafeMarkdown $label.DisplayName) | $enabledStatus | $contentType |`n" + } + $mdInfo += "`n⚠️ These labels have null or undefined EncryptionEnabled property. Verify encryption configuration manually.`n`n" + } + + if ($encryptionEnabledLabels.Count -eq 0 -and $undeterminedLabels.Count -eq 0) { + $mdInfo += "⚠️ **Recommendation**: Create at least one sensitivity label with encryption enabled to protect high-value data.`n`n" + $mdInfo += "Encryption ensures that only authorized users and applications can decrypt content, preventing data exfiltration even if files are leaked, stolen, or improperly shared.`n" + } + + $testResultMarkdown += $mdInfo + #endregion Report Generation + + $params = @{ + TestId = '35013' + Title = 'Encryption-enabled sensitivity labels are configured' + Status = $passed + Result = $testResultMarkdown + } + if ($customStatus) { + $params.CustomStatus = $customStatus + } + Add-ZtTestResultDetail @params +} From b1f861f4256a6301b47f901f0abad02687d23232 Mon Sep 17 00:00:00 2001 From: aahmed-spec Date: Tue, 6 Jan 2026 14:27:58 +0530 Subject: [PATCH 06/49] removed extra lines removed extra lines --- src/powershell/tests/Test-Assessment.25395.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25395.md b/src/powershell/tests/Test-Assessment.25395.md index 0749e8589..95f150ef0 100644 --- a/src/powershell/tests/Test-Assessment.25395.md +++ b/src/powershell/tests/Test-Assessment.25395.md @@ -1,12 +1,9 @@ When organizations configure Microsoft Entra Private Access with broad application segments—such as wide IP ranges, multiple protocols, or Quick Access configurations—they effectively replicate the over-permissive access model of traditional VPNs. This approach contradicts the Zero Trust principle of least-privilege access, where users should only reach the specific resources required for their role. Threat actors who compromise a user's credentials or device can leverage these broad network permissions to perform reconnaissance, identifying additional systems and services within the permitted range. - With visibility into the network topology, they can escalate privileges by targeting vulnerable systems, move laterally to access sensitive data stores or administrative interfaces, and establish persistence by deploying backdoors across multiple accessible systems. The lack of granular segmentation also complicates incident response, as security teams cannot quickly determine which specific resources a compromised identity could access. By contrast, per-application segmentation with tightly scoped destination hosts, specific ports, and Custom Security Attributes enables dynamic, attribute-driven Conditional Access enforcement—requiring stronger authentication or device compliance for high-risk applications while streamlining access to lower-risk resources. - This approach aligns with the Zero Trust "verify explicitly" principle by ensuring each access request is evaluated against the specific security requirements of the target application rather than applying uniform policies to broad network segments. - **Remediation action** - [Transition from Quick Access](https://learn.microsoft.com/en-us/entra/global-secure-access/how-to-configure-per-app-access) to per-app Private Access by creating individual Global Secure Access enterprise applications with specific FQDNs, IP addresses, and ports for each private resource. - [Use Application Discovery](https://learn.microsoft.com/en-us/entra/global-secure-access/how-to-application-discovery) to identify which resources users access through Quick Access, then create targeted Private Access apps for those resources. From 408cd8e0f4cf6c9de1be0d1c3de9b9ae62124ecf Mon Sep 17 00:00:00 2001 From: Manoj Kesana Date: Tue, 6 Jan 2026 15:07:52 +0530 Subject: [PATCH 07/49] Update src/powershell/tests/Test-Assessment.35009.ps1 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/powershell/tests/Test-Assessment.35009.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/powershell/tests/Test-Assessment.35009.ps1 b/src/powershell/tests/Test-Assessment.35009.ps1 index b77e6fedb..8b4c7d9a5 100644 --- a/src/powershell/tests/Test-Assessment.35009.ps1 +++ b/src/powershell/tests/Test-Assessment.35009.ps1 @@ -1,6 +1,6 @@ <# .SYNOPSIS - +Checks whether co-authoring is enabled for encrypted documents with sensitivity labels. #> function Test-Assessment-35009 { From 608ceef45940663546033f91f16e2f4f8a339530 Mon Sep 17 00:00:00 2001 From: Manoj Kesana Date: Tue, 6 Jan 2026 15:08:54 +0530 Subject: [PATCH 08/49] Update src/powershell/tests/Test-Assessment.35009.ps1 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/powershell/tests/Test-Assessment.35009.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/powershell/tests/Test-Assessment.35009.ps1 b/src/powershell/tests/Test-Assessment.35009.ps1 index 8b4c7d9a5..20b31e4f3 100644 --- a/src/powershell/tests/Test-Assessment.35009.ps1 +++ b/src/powershell/tests/Test-Assessment.35009.ps1 @@ -31,7 +31,7 @@ function Test-Assessment-35009 { } catch { Write-PSFMessage "Failed to retrieve policy configuration: $_" -Tag Test -Level Warning - Add-ZtTestResultDetail -SkippedBecause NotConnectedExchangeOnline + Add-ZtTestResultDetail -SkippedBecause NotConnectedSecurityCompliance return } From c7f977e70d70c01cd01b8d552b0068769af1377e Mon Sep 17 00:00:00 2001 From: Manoj K Date: Tue, 6 Jan 2026 22:29:01 +0530 Subject: [PATCH 09/49] Fixed the evaluation logic --- .../tests/Test-Assessment.35009.ps1 | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.35009.ps1 b/src/powershell/tests/Test-Assessment.35009.ps1 index 20b31e4f3..447e8948d 100644 --- a/src/powershell/tests/Test-Assessment.35009.ps1 +++ b/src/powershell/tests/Test-Assessment.35009.ps1 @@ -25,48 +25,55 @@ function Test-Assessment-35009 { $activity = "Checking co-authoring is enabled for encrypted documents" Write-ZtProgress -Activity $activity -Status "Getting policy configuration" + $cmdletFailed = $false + # Q1: Retrieve policy configuration settings try { $policyConfig = Get-PolicyConfig -ErrorAction Stop } catch { Write-PSFMessage "Failed to retrieve policy configuration: $_" -Tag Test -Level Warning - Add-ZtTestResultDetail -SkippedBecause NotConnectedSecurityCompliance - return + $cmdletFailed = $true } # Q2: Check EnableLabelCoauth property value - $enableLabelCoauth = $policyConfig.EnableLabelCoauth + if (-not $cmdletFailed) { + $enableLabelCoauth = $policyConfig.EnableLabelCoauth + } #endregion Data Collection #region Assessment Logic - if ($enableLabelCoauth -eq $true) { + if ($cmdletFailed) { + # Cmdlet failed - mark as Investigate + $passed = $false + $customStatus = 'Investigate' + $testResultMarkdown = "⚠️ Policy configuration exists but EnableLabelCoauth setting cannot be determined.`n`n" + } + elseif ($enableLabelCoauth -eq $true) { $passed = $true $testResultMarkdown = "✅ Co-authoring is enabled for encrypted documents with sensitivity labels.`n`n%TestResult%" } - elseif ($enableLabelCoauth -eq $false) { + else{ $passed = $false $testResultMarkdown = "❌ Co-authoring is disabled for encrypted documents.`n`n%TestResult%" } - else { - $passed = $false - $customStatus = 'Investigate' - $testResultMarkdown = "⚠️ Policy configuration exists but EnableLabelCoauth setting cannot be determined.`n`n%TestResult%" - } #endregion Assessment Logic #region Report Generation - $reportDetails = "" - $reportDetails += "`n`n## Configuration Details`n`n" - $reportDetails += "| Setting | Status |`n" - $reportDetails += "| :------ | :----- |`n" - $statusDisplay = if ($enableLabelCoauth -eq $true) { '✅ Enabled' } elseif ($enableLabelCoauth -eq $false) { '❌ Disabled' } else { '-' } - $reportDetails += "| EnableLabelCoauth | $statusDisplay |`n" - - $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $reportDetails + + if (-not $cmdletFailed) { + $reportDetails = "" + $reportDetails += "`n`n## Configuration Details`n`n" + $reportDetails += "| Setting | Status |`n" + $reportDetails += "| :------ | :----- |`n" + $statusDisplay = if ($enableLabelCoauth -eq $true) { '✅ Enabled' } elseif ($enableLabelCoauth -eq $false) { '❌ Disabled' } else { '-' } + $reportDetails += "| EnableLabelCoauth | $statusDisplay |`n" + + $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $reportDetails + } #endregion Report Generation From fd0029ea3bf39ae012d38a4fe0c9dbe1e8496b85 Mon Sep 17 00:00:00 2001 From: Sandeep Jha Date: Wed, 7 Jan 2026 03:18:16 +0530 Subject: [PATCH 10/49] refactored report table --- .../tests/Test-Assessment.25408.ps1 | 128 ++++++++---------- 1 file changed, 55 insertions(+), 73 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25408.ps1 b/src/powershell/tests/Test-Assessment.25408.ps1 index e44148f6d..7df5ef660 100644 --- a/src/powershell/tests/Test-Assessment.25408.ps1 +++ b/src/powershell/tests/Test-Assessment.25408.ps1 @@ -41,7 +41,11 @@ function Test-Assessment-25408 { Write-ZtProgress -Activity $activity -Status 'Querying filtering profiles' # Q2: Get all filtering profiles with their policies and priority - $securitySettings = Invoke-ZtGraphRequest -RelativeUri "networkAccess/filteringProfiles?`$select=id,name,description,state,version,priority&`$expand=policies(`$select=id,state;`$expand=policy(`$select=id,name,version))" -ApiVersion beta + $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 Write-ZtProgress -Activity $activity -Status 'Querying Conditional Access policies' @@ -58,7 +62,7 @@ function Test-Assessment-25408 { # Check if any Web Content Filtering policies exist (excluding "All Websites") if (-not $wcfPolicies -or $wcfPolicies.Count -eq 0) { - $testResultMarkdown = '❌ No Web Content Filtering policies are configured in the tenant. Organizations should configure web content filtering to control access to websites based on categories, domains, or URLs.' + $testResultMarkdown = '❌ Web Content Filtering policy is not configured.' $passed = $false } else { @@ -69,11 +73,11 @@ function Test-Assessment-25408 { $policyAction = $wcfPolicy.action # Find profiles that have this policy linked - $linkedSettings = @() + $linkedProfiles = @() - foreach ($securityItem in $securitySettings) { + foreach ($filteringProfile in $filteringProfiles) { # Check if this profile contains the WCF policy - $policyLink = $securityItem.policies | Where-Object { + $policyLink = $filteringProfile.policies | Where-Object { $_.'@odata.type' -eq '#microsoft.graph.networkaccess.filteringPolicyLink' -and $_.policy.id -eq $policyId -and $_.state -eq 'enabled' @@ -81,50 +85,50 @@ function Test-Assessment-25408 { if ($policyLink) { # Determine profile type based on priority - $itemType = if ($securityItem.priority -eq 65000) { 'Baseline Profile' } else { 'Security Profile' } - - $itemInfo = [PSCustomObject]@{ - ProfileId = $securityItem.id - ProfileName = $securityItem.name - ProfileType = $itemType - ProfileState = $securityItem.state - ProfilePriority = $securityItem.priority + $profileType = if ($null -ne $filteringProfile.priority -and $filteringProfile.priority -eq 65000) { 'Baseline Profile' } elseif ($null -ne $filteringProfile.priority -and $filteringProfile.priority -lt 65000) { 'Security Profile' } + + $profileInfo = [PSCustomObject]@{ + ProfileId = $filteringProfile.id + ProfileName = $filteringProfile.name + ProfileType = $profileType + ProfileState = $filteringProfile.state + ProfilePriority = $filteringProfile.priority PolicyLinkState = $policyLink.state IsApplied = $false CAPolicy = $null } # If Baseline Profile and enabled, it's automatically applied - if ($itemType -eq 'Baseline Profile' -and $securityItem.state -eq 'enabled') { - $itemInfo.IsApplied = $true + if ($profileType -eq 'Baseline Profile' -and $filteringProfile.state -eq 'enabled') { + $profileInfo.IsApplied = $true } # If Security Profile, check if it's linked to an active CA policy - elseif ($itemType -eq 'Security Profile' -and $securityItem.state -eq 'enabled') { + elseif ($profileType -eq 'Security Profile' -and $filteringProfile.state -eq 'enabled') { # Step 4: Check for Conditional Access policy linkage $linkedCAPolicies = $caPolicies | Where-Object { $_.state -eq 'enabled' -and $null -ne $_.sessionControls.globalSecureAccessFilteringProfile -and - $_.sessionControls.globalSecureAccessFilteringProfile.profileId -eq $securityItem.id -and + $_.sessionControls.globalSecureAccessFilteringProfile.profileId -eq $filteringProfile.id -and $_.sessionControls.globalSecureAccessFilteringProfile.isEnabled -eq $true } if ($linkedCAPolicies) { - $itemInfo.IsApplied = $true - $itemInfo.CAPolicy = $linkedCAPolicies + $profileInfo.IsApplied = $true + $profileInfo.CAPolicy = $linkedCAPolicies } } - $linkedSettings += $itemInfo + $linkedProfiles += $profileInfo } } # If this policy is applied through at least one profile, add it to applied policies - if ($linkedSettings | Where-Object { $_.IsApplied -eq $true }) { + if ($linkedProfiles | Where-Object { $_.IsApplied -eq $true }) { $appliedPolicies += [PSCustomObject]@{ PolicyId = $policyId PolicyName = $policyName PolicyAction = $policyAction - LinkedSettings = $linkedSettings + LinkedProfiles = $linkedProfiles } } } @@ -132,77 +136,55 @@ function Test-Assessment-25408 { # Determine pass/fail if ($appliedPolicies.Count -gt 0) { $passed = $true - $testResultMarkdown = "✅ Web Content Filtering policies are enabled and configured. `n`n%TestResult%" + $testResultMarkdown = "✅ Web Content Filtering policy is applied. `n`n%TestResult%" } else { $passed = $false - $testResultMarkdown = "❌ Web Content Filtering policies exist but are not applied to any users through Baseline Profile or Security Profiles linked to Conditional Access policies. `n`n%TestResult%" + $testResultMarkdown = "❌ Web Content Filtering policy is not applied to users. `n`n%TestResult%" } } #endregion Assessment Logic #region Report Generation # Build detailed markdown information - $portalLink = 'https://entra.microsoft.com/#view/Microsoft_AAD_IAM/CompanyNetworkAccessMenuBlade/~/GlobalSecureAccessWebContentFiltering' $mdInfo = '' if ($wcfPolicies -and $wcfPolicies.Count -gt 0) { - # Show all WCF policies and their application status - $mdInfo += "## [Web Content Filtering Policies]($portalLink)`n`n" + $mdInfo += "### Web Content Filtering Configuration`n`n" + $mdInfo += "| Policy name | Policy state | Linked profile name | Linked profile priority | Linked profile state | CA policy name | CA policy state |`n" + $mdInfo += "|-------------|--------------|---------------------|-------------------------|----------------------|----------------|-----------------|`n" - if ($appliedPolicies.Count -gt 0) { - foreach ($appliedPolicy in $appliedPolicies) { - $safePolicyName = Get-SafeMarkdown $appliedPolicy.PolicyName - $mdInfo += "### ✅ $safePolicyName`n`n" - $mdInfo += "**Action**: $($appliedPolicy.PolicyAction)`n`n" - - # Show profiles this policy is linked to - $appliedItems = $appliedPolicy.LinkedSettings | Where-Object { $_.IsApplied -eq $true } - - foreach ($itemInfo in $appliedItems) { - $safeName = Get-SafeMarkdown $itemInfo.ProfileName - $itemBladeLink = "https://entra.microsoft.com/#view/Microsoft_AAD_IAM/CompanyNetworkAccessMenuBlade/~/GlobalSecureAccessInternetAccessProfiles" - - $mdInfo += "#### [$safeName]($itemBladeLink) ($($itemInfo.ProfileType))`n`n" - $mdInfo += "| Property | Value |`n" - $mdInfo += "|----------|-------|`n" - $mdInfo += "| Profile Id | ``$($itemInfo.ProfileId)`` |`n" - $mdInfo += "| Priority | $($itemInfo.ProfilePriority) |`n" - $mdInfo += "| State | $($itemInfo.ProfileState) |`n" - $mdInfo += "| Policy link state | $($itemInfo.PolicyLinkState) |`n" - - # If linked via CA policy, show CA policy details - if ($itemInfo.CAPolicy) { - $mdInfo += "`n**Applied through Conditional Access Policies:**`n`n" - $mdInfo += "| Policy name | State | Id |`n" - $mdInfo += "|-------------|-------|-----|`n" - - foreach ($caPolicy in $itemInfo.CAPolicy) { + foreach ($wcfPolicy in $wcfPolicies) { + $safePolicyName = Get-SafeMarkdown $wcfPolicy.name + $appliedPolicy = $appliedPolicies | Where-Object { $_.PolicyId -eq $wcfPolicy.id } + + if ($appliedPolicy) { + # Get applied profiles for this policy + $appliedProfiles = $appliedPolicy.LinkedProfiles | Where-Object { $_.IsApplied -eq $true } + + foreach ($profileInfo in $appliedProfiles) { + $safeProfileName = Get-SafeMarkdown $profileInfo.ProfileName + $profilePriority = $profileInfo.ProfilePriority + $profileState = $profileInfo.ProfileState + $policyLinkState = $profileInfo.PolicyLinkState + + # If there are CA policies, create a row for each one + if ($profileInfo.CAPolicy -and $profileInfo.CAPolicy.Count -gt 0) { + foreach ($caPolicy in $profileInfo.CAPolicy) { $safeCAPolicyName = Get-SafeMarkdown $caPolicy.displayName - $caPolicyLink = "https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/PolicyBlade/policyId/$($caPolicy.id)" - $mdInfo += "| [$safeCAPolicyName]($caPolicyLink) | $($caPolicy.state) | ``$($caPolicy.id)`` |`n" + $caPolicyState = $caPolicy.state + $mdInfo += "| $safePolicyName | $policyLinkState | $safeProfileName | $profilePriority | $profileState | $safeCAPolicyName | $caPolicyState |`n" } } else { - $mdInfo += "`n*Applied through Baseline Profile (automatically applies to all users)*`n" + # Baseline profile or profile without CA policy + $mdInfo += "| $safePolicyName | $policyLinkState | $safeProfileName | $profilePriority | $profileState | Auto-applied (Baseline) | - |`n" } - - $mdInfo += "`n" } } - } - - # Show unapplied policies if any - $unappliedPolicies = $wcfPolicies | Where-Object { $_.id -notin $appliedPolicies.PolicyId } - if ($unappliedPolicies.Count -gt 0) { - $mdInfo += "### ❌ Policies Not Applied to Users`n`n" - $mdInfo += "The following Web Content Filtering policies exist but are not applied through any profile:`n`n" - $mdInfo += "| Policy name | Action | Id |`n" - $mdInfo += "|-------------|--------|-----|`n" - - foreach ($unappliedPolicy in $unappliedPolicies) { - $safeName = Get-SafeMarkdown $unappliedPolicy.name - $mdInfo += "| $safeName | $($unappliedPolicy.action) | ``$($unappliedPolicy.id)`` |`n" + else { + # Policy not applied - show with empty profile/CA info + $mdInfo += "| $safePolicyName | Not applied | - | - | - | - | - |`n" } } } From 67086955861b722e106a51ab762edeaf2cf914f0 Mon Sep 17 00:00:00 2001 From: Sandeep Jha Date: Wed, 7 Jan 2026 08:03:45 +0530 Subject: [PATCH 11/49] adding blade links to profiles and policies --- .../tests/Test-Assessment.25408.ps1 | 98 ++++++++++++------- 1 file changed, 60 insertions(+), 38 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25408.ps1 b/src/powershell/tests/Test-Assessment.25408.ps1 index 7df5ef660..0cb6365f3 100644 --- a/src/powershell/tests/Test-Assessment.25408.ps1 +++ b/src/powershell/tests/Test-Assessment.25408.ps1 @@ -57,7 +57,6 @@ function Test-Assessment-25408 { # Initialize test variables $testResultMarkdown = '' $passed = $false - $customStatus = $null $appliedPolicies = @() # Check if any Web Content Filtering policies exist (excluding "All Websites") @@ -136,7 +135,7 @@ function Test-Assessment-25408 { # Determine pass/fail if ($appliedPolicies.Count -gt 0) { $passed = $true - $testResultMarkdown = "✅ Web Content Filtering policy is applied. `n`n%TestResult%" + $testResultMarkdown = "✅ Web Content Filtering policy is enabled. `n`n%TestResult%" } else { $passed = $false @@ -150,41 +149,69 @@ function Test-Assessment-25408 { $mdInfo = '' if ($wcfPolicies -and $wcfPolicies.Count -gt 0) { - $mdInfo += "### Web Content Filtering Configuration`n`n" - $mdInfo += "| Policy name | Policy state | Linked profile name | Linked profile priority | Linked profile state | CA policy name | CA policy state |`n" - $mdInfo += "|-------------|--------------|---------------------|-------------------------|----------------------|----------------|-----------------|`n" - - foreach ($wcfPolicy in $wcfPolicies) { - $safePolicyName = Get-SafeMarkdown $wcfPolicy.name - $appliedPolicy = $appliedPolicies | Where-Object { $_.PolicyId -eq $wcfPolicy.id } - - if ($appliedPolicy) { - # Get applied profiles for this policy - $appliedProfiles = $appliedPolicy.LinkedProfiles | Where-Object { $_.IsApplied -eq $true } - - foreach ($profileInfo in $appliedProfiles) { - $safeProfileName = Get-SafeMarkdown $profileInfo.ProfileName - $profilePriority = $profileInfo.ProfilePriority - $profileState = $profileInfo.ProfileState - $policyLinkState = $profileInfo.PolicyLinkState - - # If there are CA policies, create a row for each one - if ($profileInfo.CAPolicy -and $profileInfo.CAPolicy.Count -gt 0) { - foreach ($caPolicy in $profileInfo.CAPolicy) { - $safeCAPolicyName = Get-SafeMarkdown $caPolicy.displayName - $caPolicyState = $caPolicy.state - $mdInfo += "| $safePolicyName | $policyLinkState | $safeProfileName | $profilePriority | $profileState | $safeCAPolicyName | $caPolicyState |`n" + # Check if there are any applied policies to determine table structure + if ($appliedPolicies.Count -gt 0) { + # Add table title for applied policies + $mdInfo += "### Applied web content filtering policies`n`n" + + # Full table with all columns + $mdInfo += "| Linked profile name | Linked profile priority | Linked policy name | Policy state | Profile state | Policy action | CA policy name | CA policy state |`n" + $mdInfo += "|---------------------|-------------------------|--------------------|--------------|---------------|---------------|----------------|-----------------|`n" + + foreach ($wcfPolicy in $wcfPolicies | Sort-Object -Property name) { + $safePolicyName = Get-SafeMarkdown $wcfPolicy.name + $policyAction = $wcfPolicy.action + $appliedPolicy = $appliedPolicies | Where-Object { $_.PolicyId -eq $wcfPolicy.id } + + if ($appliedPolicy) { + # Get applied profiles for this policy + $appliedProfiles = $appliedPolicy.LinkedProfiles | Where-Object { $_.IsApplied -eq $true } + + foreach ($profileInfo in $appliedProfiles) { + $safeProfileName = Get-SafeMarkdown $profileInfo.ProfileName + $profilePriority = $profileInfo.ProfilePriority + $profileState = $profileInfo.ProfileState + $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)" + + # If there are CA policies, create a row for each one + if ($profileInfo.CAPolicy -and $profileInfo.CAPolicy.Count -gt 0) { + foreach ($caPolicy in $profileInfo.CAPolicy) { + $caPolicyPortalLink = "https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/PolicyBlade/policyId/$($caPolicy.id)" + $safeCAPolicyName = Get-SafeMarkdown $caPolicy.displayName + $caPolicyNameWithLink = "[$safeCAPolicyName]($caPolicyPortalLink)" + $caPolicyState = $caPolicy.state + + $mdInfo += "| $profileNameWithLink | $profilePriority | $policyNameWithLink | $policyLinkState | $profileState | $policyAction | $caPolicyNameWithLink | $caPolicyState |`n" + } + } + else { + # Baseline profile or profile without CA policy + $mdInfo += "| $profileNameWithLink | $profilePriority | $policyNameWithLink | $policyLinkState | $profileState | $policyAction | Not applicable | Not applicable |`n" } - } - else { - # Baseline profile or profile without CA policy - $mdInfo += "| $safePolicyName | $policyLinkState | $safeProfileName | $profilePriority | $profileState | Auto-applied (Baseline) | - |`n" } } } - else { - # Policy not applied - show with empty profile/CA info - $mdInfo += "| $safePolicyName | Not applied | - | - | - | - | - |`n" + } + else { + # Add table title with blade link for unapplied policies + $mdInfo += "### [Web content filtering policies](https://entra.microsoft.com/#view/Microsoft_Azure_Network_Access/WebFilteringPolicy.ReactView)`n`n" + + # Simple table with only policy info + $mdInfo += "The following Web Content Filtering policies are configured but not applied to users.`n`n" + $mdInfo += "| Policy name | Policy action |`n" + $mdInfo += "|-------------|---------------|`n" + + foreach ($wcfPolicy in $wcfPolicies | Sort-Object -Property name) { + $safePolicyName = Get-SafeMarkdown $wcfPolicy.name + $policyAction = $wcfPolicy.action + $mdInfo += "| $safePolicyName | $policyAction |`n" } } } @@ -200,11 +227,6 @@ function Test-Assessment-25408 { Result = $testResultMarkdown } - # Add CustomStatus if needed - if ($null -ne $customStatus) { - $params.CustomStatus = $customStatus - } - # Add test result details Add-ZtTestResultDetail @params } From ae8e04913f68b06393195c76fe623fa56cb571fe Mon Sep 17 00:00:00 2001 From: Sandeep Jha Date: Wed, 7 Jan 2026 08:22:32 +0530 Subject: [PATCH 12/49] updated comments and word capitalization --- src/powershell/tests/Test-Assessment.25408.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25408.ps1 b/src/powershell/tests/Test-Assessment.25408.ps1 index 0cb6365f3..597d58275 100644 --- a/src/powershell/tests/Test-Assessment.25408.ps1 +++ b/src/powershell/tests/Test-Assessment.25408.ps1 @@ -154,7 +154,7 @@ function Test-Assessment-25408 { # Add table title for applied policies $mdInfo += "### Applied web content filtering policies`n`n" - # Full table with all columns + # table for applied policies $mdInfo += "| Linked profile name | Linked profile priority | Linked policy name | Policy state | Profile state | Policy action | CA policy name | CA policy state |`n" $mdInfo += "|---------------------|-------------------------|--------------------|--------------|---------------|---------------|----------------|-----------------|`n" @@ -203,8 +203,8 @@ function Test-Assessment-25408 { # Add table title with blade link for unapplied policies $mdInfo += "### [Web content filtering policies](https://entra.microsoft.com/#view/Microsoft_Azure_Network_Access/WebFilteringPolicy.ReactView)`n`n" - # Simple table with only policy info - $mdInfo += "The following Web Content Filtering policies are configured but not applied to users.`n`n" + # table for unapplied policies + $mdInfo += "The following web content filtering policies are configured but not applied to users.`n`n" $mdInfo += "| Policy name | Policy action |`n" $mdInfo += "|-------------|---------------|`n" From bc856ef43b3e5fe1ba562689ed423e07a3a4f4cb Mon Sep 17 00:00:00 2001 From: Komal Date: Wed, 7 Jan 2026 12:53:05 +0530 Subject: [PATCH 13/49] add support for AIPService --- .../public/Connect-ZtAssessment.ps1 | 44 +++++- .../tests/Test-Assessment.35011.ps1 | 144 ++++++++++++++++++ 2 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 src/powershell/tests/Test-Assessment.35011.ps1 diff --git a/src/powershell/public/Connect-ZtAssessment.ps1 b/src/powershell/public/Connect-ZtAssessment.ps1 index 9d66eeb90..3b23b6caa 100644 --- a/src/powershell/public/Connect-ZtAssessment.ps1 +++ b/src/powershell/public/Connect-ZtAssessment.ps1 @@ -83,7 +83,7 @@ $SkipAzureConnection, # The services to connect to such as Azure and ExchangeOnline. Default is Graph. - [ValidateSet('All', 'Azure', 'ExchangeOnline', 'Graph', 'SecurityCompliance', 'SharePointOnline')] + [ValidateSet('All', 'Azure', 'AipService', 'ExchangeOnline', 'Graph', 'SecurityCompliance', 'SharePointOnline')] [string[]]$Service = 'Graph', # The Exchange environment to connect to. Default is O365Default. Supported values include O365China, O365Default, O365GermanyCloud, O365USGovDoD, O365USGovGCCHigh. @@ -117,7 +117,7 @@ } - $OrderedImport = Get-ModuleImportOrder -Name @('Az.Accounts', 'ExchangeOnlineManagement', 'Microsoft.Graph.Authentication', 'Microsoft.Online.SharePoint.PowerShell') + $OrderedImport = Get-ModuleImportOrder -Name @('Az.Accounts', 'ExchangeOnlineManagement', 'Microsoft.Graph.Authentication', 'Microsoft.Online.SharePoint.PowerShell', 'AipService') Write-Verbose "Import Order: $($OrderedImport.Name -join ', ')" @@ -348,6 +348,32 @@ } } } + + 'AipService' { + if ($Service -contains 'AipService' -or $Service -contains 'All') { + try { + # Import module with compatibility if needed + if ($PSVersionTable.PSEdition -ne 'Desktop') { + # Assume module is installed in Windows PowerShell as per instructions + Import-Module AipService -UseWindowsPowerShell -WarningAction SilentlyContinue -ErrorAction Stop -Global + } + else { + Import-Module AipService -ErrorAction Stop -Global + } + } + catch { + # Provide clearer guidance when import fails, especially under PowerShell Core + if ($PSVersionTable.PSEdition -ne 'Desktop') { + $message = "Failed to import AipService module. When running in PowerShell Core, 'AipService' must be installed in Windows PowerShell 5.1 (Desktop) for -UseWindowsPowerShell to work. Underlying error: $_" + } + else { + $message = "Failed to import AipService module: $_" + } + Write-Host "`n$message" -ForegroundColor Red + Write-PSFMessage $message -Level Error + } + } + } } if ($Service -contains 'SharePointOnline' -or $Service -contains 'All') { @@ -388,4 +414,18 @@ } } } + + if ($Service -contains 'AipService' -or $Service -contains 'All') { + Write-Host "`nConnecting to Azure Information Protection" -ForegroundColor Yellow + Write-PSFMessage 'Connecting to Azure Information Protection' + + try { + Connect-AipService -ErrorAction Stop + Write-Verbose "Successfully connected to Azure Information Protection." + } + catch { + Write-Host "`nFailed to connect to Azure Information Protection: $_" -ForegroundColor Red + Write-PSFMessage "Failed to connect to Azure Information Protection: $_" -Level Error + } + } } diff --git a/src/powershell/tests/Test-Assessment.35011.ps1 b/src/powershell/tests/Test-Assessment.35011.ps1 new file mode 100644 index 000000000..c6a5591ea --- /dev/null +++ b/src/powershell/tests/Test-Assessment.35011.ps1 @@ -0,0 +1,144 @@ +<# +.SYNOPSIS + Azure Information Protection (AIP) Super User Feature Configuration + +.DESCRIPTION + Evaluates whether the Azure Information Protection (AIP) super user feature is enabled and properly configured with designated super users. The super user feature allows specified service accounts or administrators to decrypt rights-managed content for auditing, search, and compliance purposes. + + The cmdlets require the AipService module (v3.0+) which is only supported on Windows PowerShell 5.1. A PowerShell 7 subprocess workaround is automatically employed if running under PowerShell Core. + +.NOTES + Test ID: 35011 + Pillar: Data + Risk Level: High +#> + +function Test-Assessment-35011 { + [ZtTest( + Category = 'Azure Information Protection', + ImplementationCost = 'Medium', + MinimumLicense = ('Microsoft 365 E5'), + Pillar = 'Data', + RiskLevel = 'High', + SfiPillar = '', + TenantType = ('Workforce','External'), + TestId = 35011, + Title = 'Azure Information Protection (AIP) Super User Feature', + UserImpact = 'Low' + )] + [CmdletBinding()] + param() + + #region Data Collection + Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose + + $activity = 'Checking Azure Information Protection Super User Configuration' + Write-ZtProgress -Activity $activity -Status 'Querying AIP super user settings' + + $superUserFeatureEnabled = $null + $superUsers = @() + $errorMsg = $null + + try { + # Note: AipService must be authenticated in Connect-ZtAssessment first + # This test only performs queries against the authenticated service + + # Query Q1: Check if super user feature is enabled + $superUserFeatureEnabled = Get-AipServiceSuperUserFeature -ErrorAction Stop + + # Query Q2: Get list of configured super users + $superUsers = Get-AipServiceSuperUser -ErrorAction Stop + } + catch { + $errorMsg = $_ + Write-PSFMessage "Error querying AIP Super User configuration: $_" -Level Error + } + #endregion Data Collection + + #region Assessment Logic + $passed = $false + $investigateFlag = $false + + if ($errorMsg) { + $investigateFlag = $true + } + else { + # Evaluation logic: + # 1. If feature is disabled, test fails + if ($superUserFeatureEnabled -eq $false) { + $passed = $false + } + # 2. If feature is enabled, check if at least one super user is configured + elseif ($superUserFeatureEnabled -eq $true) { + $superUserCount = if ($superUsers) { @($superUsers).Count } else { 0 } + + if ($superUserCount -ge 1) { + $passed = $true + } + else { + $passed = $false + } + } + } + #endregion Assessment Logic + + #region Report Generation + $testResultMarkdown = "" + + if ($investigateFlag) { + $testResultMarkdown = "⚠️ Unable to determine AIP super user configuration due to permissions or connection issues.`n`n" + $testResultMarkdown += "**Error Details:**`n" + $testResultMarkdown += "* $errorMsg`n`n" + $testResultMarkdown += "**Possible Causes:**`n" + $testResultMarkdown += "* AipService module not installed (requires v3.0+)`n" + $testResultMarkdown += "* Not connected to AIP service`n" + $testResultMarkdown += "* Insufficient permissions to query AIP configuration`n" + } + else { + if ($passed) { + $testResultMarkdown = "✅ Super user feature is enabled with at least one member configured.`n`n" + } + else { + if ($superUserFeatureEnabled -eq $true) { + $testResultMarkdown = "❌ Super user feature is enabled BUT no members are configured.`n`n" + } + else { + $testResultMarkdown = "❌ Super user feature is DISABLED.`n`n" + } + } + + $testResultMarkdown += "### Azure Information Protection Super User Configuration`n`n" + $testResultMarkdown += "**Feature Status:**`n" + + $featureStatus = if ($superUserFeatureEnabled) { "Enabled" } else { "Disabled" } + $testResultMarkdown += "* Super User Feature: $featureStatus`n`n" + + if ($superUserFeatureEnabled) { + $superUserCount = if ($superUsers) { @($superUsers).Count } else { 0 } + $testResultMarkdown += "**Super Users Configured: $superUserCount**`n`n" + + if ($superUserCount -gt 0) { + $testResultMarkdown += "| Email Address / Service Principal ID | Account Type |`n" + $testResultMarkdown += "| :--- | :--- |`n" + + foreach ($superUser in $superUsers) { + $accountType = if ($superUser -like '*-*-*-*-*') { "Service Principal" } else { "User" } + $testResultMarkdown += "| $superUser | $accountType |`n" + } + + $testResultMarkdown += "`n" + } + } + + $testResultMarkdown += "**Note:** Super user configuration is not available through the Azure portal and must be managed via PowerShell using the AipService module.`n" + } + #endregion Report Generation + + $testResultDetail = @{ + TestId = '35011' + Title = 'Azure Information Protection (AIP) Super User Feature' + Status = $passed + Result = $testResultMarkdown + } + Add-ZtTestResultDetail @testResultDetail +} From 9d6b6dce276c235e8493d3c98448abfe7da7ddcf Mon Sep 17 00:00:00 2001 From: Komal Date: Wed, 7 Jan 2026 13:45:19 +0530 Subject: [PATCH 14/49] add md file --- src/powershell/tests/Test-Assessment.35011.md | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/powershell/tests/Test-Assessment.35011.md diff --git a/src/powershell/tests/Test-Assessment.35011.md b/src/powershell/tests/Test-Assessment.35011.md new file mode 100644 index 000000000..92b9d8f5a --- /dev/null +++ b/src/powershell/tests/Test-Assessment.35011.md @@ -0,0 +1,27 @@ +The super user feature in Azure Information Protection grants designated accounts the ability to decrypt all content protected by the organization's Rights Management service, regardless of the encryption permissions originally assigned. Super users can access encrypted documents even when they are not explicitly granted permissions by the content owner, enabling scenarios such as eDiscovery, data recovery, compliance investigations, and migration from encrypted content. + +Without super user configuration, organizations risk data loss when encryption keys become inaccessible, employees leave without transferring ownership of critical encrypted files, or legal holds require access to protected content where the original rights holders cannot be reached. The super user feature must be explicitly enabled and membership must be carefully controlled—typically limited to service accounts used by compliance tools, backup systems, or eDiscovery platforms rather than individual user accounts. Failure to configure super users creates operational risk where encrypted content becomes permanently inaccessible, while overly broad super user membership creates security risk where unauthorized accounts gain unrestricted access to all protected content. + +**Remediation action** + +To configure super users: + +Connect to Azure Information Protection PowerShell: Connect-AipService +Enable the super user feature: Enable-AipServiceSuperUserFeature +Add super users (service accounts recommended): +For user accounts: Add-AipServiceSuperUser -EmailAddress "serviceaccount@contoso.com" +For service principals: Add-AipServiceSuperUser -ServicePrincipalId "service-principal-id" +Verify configuration: Get-AipServiceSuperUser + +Best practices: + +- Limit super user membership to dedicated service accounts +- Use service principals for automated tools (eDiscovery, backup) +- Avoid assigning super user to individual employee accounts +- Audit super user access regularly +- Document business justification for each super user account + +[Configure super users for Azure Information Protection Enable-AipServiceSuperUserFeature Add-AipServiceSuperUser](https://learn.microsoft.com/en-us/powershell/module/aipservice/enable-aipservicesuperuserfeature) + + +%TestResult% From f6bee364daef2ab8c6db9d6272c817766ab4088f Mon Sep 17 00:00:00 2001 From: Ashwini Karke Date: Wed, 7 Jan 2026 14:20:01 +0530 Subject: [PATCH 15/49] added test file --- .../tests/Test-Assessment.35025.ps1 | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 src/powershell/tests/Test-Assessment.35025.ps1 diff --git a/src/powershell/tests/Test-Assessment.35025.ps1 b/src/powershell/tests/Test-Assessment.35025.ps1 new file mode 100644 index 000000000..17ac016a6 --- /dev/null +++ b/src/powershell/tests/Test-Assessment.35025.ps1 @@ -0,0 +1,143 @@ +<# +.SYNOPSIS + Validates that internal RMS licensing is enabled in Exchange Online. + +.DESCRIPTION + This test checks if internal RMS licensing is enabled, which allows users and services within the + organization to license protected content for internal distribution and sharing. Without internal + RMS licensing enabled, users cannot share rights-protected content with internal recipients. + +.NOTES + Test ID: 35025 + Category: Data + Pillar: Data + Required Module: ExchangeOnlineManagement + Required Connection: Exchange Online +#> + +function Test-Assessment-35025 { + [ZtTest( + Category = 'Data', + ImplementationCost = 'Low', + MinimumLicense = 'Microsoft_365_E3', + Pillar = 'Data', + RiskLevel = 'High', + SfiPillar = 'Protect tenants and production systems', + TenantType = ('Workforce', 'External'), + TestId = 35025, + Title = 'Internal RMS licensing is enabled', + UserImpact = 'High' + )] + [CmdletBinding()] + param() + + #region Data Collection + Write-PSFMessage 'Start Internal RMS Licensing evaluation' -Tag Test -Level VeryVerbose + + $activity = 'Checking Internal RMS Licensing' + Write-ZtProgress -Activity $activity -Status 'Getting IRM configuration' + + # Q1: Get IRM licensing configuration + try { + $irmConfig = Get-IRMConfiguration -ErrorAction Stop + } + catch { + Write-PSFMessage "Failed to retrieve IRM configuration: $_" -Tag Test -Level Warning + + $params = @{ + TestId = '35025' + Title = 'Internal RMS licensing is enabled' + Status = $false + Result = "❌ Unable to retrieve IRM configuration. Ensure you are connected to Exchange Online with Connect-ExchangeOnline.`n`nError: $_" + } + Add-ZtTestResultDetail @params + return + } + + # Q2: Check if internal licensing is enabled + $internalLicensingEnabled = $irmConfig.InternalLicensingEnabled + + # Q3: Get detailed licensing configuration + $detailedConfig = $irmConfig | Select-Object -Property InternalLicensingEnabled, ExternalLicensingEnabled, AzureRMSLicensingEnabled + #endregion Data Collection + + #region Assessment Logic + $passed = $false + $customStatus = $null + + # Investigate: Cannot determine licensing status + if ($null -eq $internalLicensingEnabled) { + $passed = $true + $customStatus = 'Investigate' + } + # Pass: Internal RMS licensing is enabled + elseif ($internalLicensingEnabled -eq $true) { + $passed = $true + } + # Fail: Internal RMS licensing is not enabled + else { + $passed = $false + } + #endregion Assessment Logic + + #region Report Generation + $testResultMarkdown = '' + + if ($customStatus -eq 'Investigate') { + $testResultMarkdown = "⚠️ Unable to determine internal RMS licensing status due to permissions issues or incomplete configuration data.`n`n" + + $params = @{ + TestId = '35025' + Title = 'Internal RMS licensing is enabled' + Status = $passed + Result = $testResultMarkdown + CustomStatus = $customStatus + } + Add-ZtTestResultDetail @params + return + } + elseif ($passed) { + $testResultMarkdown = "✅ Internal RMS licensing is enabled, allowing internal users to license and share protected content within the organization.`n`n" + } + else { + $testResultMarkdown = "❌ Internal RMS licensing is not enabled or licensing endpoints are not configured.`n`n" + } + + # Build detailed information + $mdInfo = "## [Internal RMS Licensing Configuration](https://purview.microsoft.com/settings/encryption)`n`n" + $mdInfo += "| Setting | Status |`n" + $mdInfo += "| :------ | :----- |`n" + $mdInfo += "| Internal Licensing Enabled | $(if ($detailedConfig.InternalLicensingEnabled -eq $true) { '✅ Enabled' } elseif ($detailedConfig.InternalLicensingEnabled -eq $false) { '❌ Disabled' } else { '⚠️ Unknown' }) |`n" + $mdInfo += "| External Licensing Enabled | $(if ($detailedConfig.ExternalLicensingEnabled -eq $true) { '✅ Enabled' } elseif ($detailedConfig.ExternalLicensingEnabled -eq $false) { '❌ Disabled' } else { '⚠️ Unknown' }) |`n" + $mdInfo += "| Azure RMS Licensing Enabled | $(if ($detailedConfig.AzureRMSLicensingEnabled -eq $true) { '✅ Enabled' } elseif ($detailedConfig.AzureRMSLicensingEnabled -eq $false) { '❌ Disabled' } else { '⚠️ Unknown' }) |`n" + $mdInfo += "`n" + + # Additional configuration details + if ($irmConfig) { + $mdInfo += "## Additional Configuration Details`n`n" + + if ($irmConfig.LicensingLocation) { + $mdInfo += "- **Licensing Location**: $(Get-SafeMarkdown $irmConfig.LicensingLocation)`n" + } + + if ($irmConfig.RMSOnlineKeySharingLocation) { + $mdInfo += "- **RMS Online Key Sharing Location**: $(Get-SafeMarkdown $irmConfig.RMSOnlineKeySharingLocation)`n" + } + + $mdInfo += "`n" + } + + $testResultMarkdown += $mdInfo + #endregion Report Generation + + $params = @{ + TestId = '35025' + Title = 'Internal RMS licensing is enabled' + Status = $passed + Result = $testResultMarkdown + } + if ($customStatus) { + $params.CustomStatus = $customStatus + } + Add-ZtTestResultDetail @params +} From 0a648d944e0ecab056d7a447fbfbdb35bd7e999c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:02:12 +0000 Subject: [PATCH 16/49] Initial plan From 6a0065da4585253ffe84a520546048e53d4e3e99 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:05:34 +0000 Subject: [PATCH 17/49] Extract AD ports to constant $AD_WELL_KNOWN_PORTS Co-authored-by: aahmed-spec <250927798+aahmed-spec@users.noreply.github.com> --- src/powershell/tests/Test-Assessment.25395.ps1 | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25395.ps1 b/src/powershell/tests/Test-Assessment.25395.ps1 index 53fc74db8..6503b79bf 100644 --- a/src/powershell/tests/Test-Assessment.25395.ps1 +++ b/src/powershell/tests/Test-Assessment.25395.ps1 @@ -31,6 +31,9 @@ function Test-Assessment-25395 { [CmdletBinding()] param() + # Active Directory well-known ports + $AD_WELL_KNOWN_PORTS = @('53','88','135','389','445','464','636','3268','3269') + #region Helper Functions function Test-IsBroadCidr { @@ -98,11 +101,10 @@ function Test-Assessment-25395 { System.Boolean - True if port is a valid AD well-known port, false otherwise. #> param([string]$Port) - $valid = @('53','88','135','389','445','464','636','3268','3269') if ($Port -match '^(\d+)-(\d+)$') { - return ($matches[1] -eq $matches[2] -and $valid -contains $matches[1]) + return ($matches[1] -eq $matches[2] -and $AD_WELL_KNOWN_PORTS -contains $matches[1]) } - return ($valid -contains $Port) + return ($AD_WELL_KNOWN_PORTS -contains $Port) } #endregion Helper Functions From 1d168edc3a5c1a4af7399c8928aafd8172bde123 Mon Sep 17 00:00:00 2001 From: aahmed-spec Date: Wed, 7 Jan 2026 18:12:13 +0530 Subject: [PATCH 18/49] Update src/powershell/tests/Test-Assessment.25395.ps1 Inconsistent indentation detected. Lines 249-256 use tabs while the rest of the file uses spaces. PowerShell style guidelines typically recommend using spaces consistently throughout the file for better readability across different editors. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/powershell/tests/Test-Assessment.25395.ps1 | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25395.ps1 b/src/powershell/tests/Test-Assessment.25395.ps1 index 6503b79bf..046cc5cf8 100644 --- a/src/powershell/tests/Test-Assessment.25395.ps1 +++ b/src/powershell/tests/Test-Assessment.25395.ps1 @@ -248,14 +248,14 @@ function Test-Assessment-25395 { } $appResults += [PSCustomObject]@{ - AppName = $app.displayName - AppObjectId = $app.id - AppId = $app.appId - SegmentType = if ($segments) { ($segments.destinationType | Select-Object -Unique) -join ', ' } else { 'None' } - SegmentScope = ($segmentSummary -join ' | ') - HasCSA = [bool]$sp.customSecurityAttributes - Status = $appStatus - } + AppName = $app.displayName + AppObjectId = $app.id + AppId = $app.appId + SegmentType = if ($segments) { ($segments.destinationType | Select-Object -Unique) -join ', ' } else { 'None' } + SegmentScope = ($segmentSummary -join ' | ') + HasCSA = [bool]$sp.customSecurityAttributes + Status = $appStatus + } } From 003cabcec0c6a9c1bd7007ec9d15c58acdfeafb8 Mon Sep 17 00:00:00 2001 From: aahmed-spec Date: Wed, 7 Jan 2026 18:13:00 +0530 Subject: [PATCH 19/49] Update src/powershell/tests/Test-Assessment.25395.ps1 Inconsistent indentation detected. Line 346 uses tabs while the rest of the file uses spaces. PowerShell style guidelines typically recommend using spaces consistently throughout the file for better readability across different editors. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/powershell/tests/Test-Assessment.25395.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25395.ps1 b/src/powershell/tests/Test-Assessment.25395.ps1 index 046cc5cf8..5cfd3e29f 100644 --- a/src/powershell/tests/Test-Assessment.25395.ps1 +++ b/src/powershell/tests/Test-Assessment.25395.ps1 @@ -345,10 +345,10 @@ function Test-Assessment-25395 { $mdInfo += $formatTemplate -f $tableRows } - # Replace the placeholder with detailed information + # Replace the placeholder with detailed information $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo #endregion Report Generation - + $params = @{ TestId = '25395' Title = 'Private Access application segments enforce least-privilege access' From b0e888d6c9945ac9f4165e32f1a9783a72a63caa Mon Sep 17 00:00:00 2001 From: aahmed-spec Date: Wed, 7 Jan 2026 18:13:45 +0530 Subject: [PATCH 20/49] Update src/powershell/tests/Test-Assessment.25395.ps1 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/powershell/tests/Test-Assessment.25395.ps1 | 1 - 1 file changed, 1 deletion(-) diff --git a/src/powershell/tests/Test-Assessment.25395.ps1 b/src/powershell/tests/Test-Assessment.25395.ps1 index 5cfd3e29f..8410f2363 100644 --- a/src/powershell/tests/Test-Assessment.25395.ps1 +++ b/src/powershell/tests/Test-Assessment.25395.ps1 @@ -151,7 +151,6 @@ function Test-Assessment-25395 { $appsWithoutCSA = @() $segmentFindings = @() $appResults = @() - # Step 1: Check if any per-app Private Access applications exist if ($null -ne $apps -and $apps.Count -gt 0) { From a621f44304236eb71c4b638fc442eafc32c084e0 Mon Sep 17 00:00:00 2001 From: Sandeep Jha Date: Wed, 7 Jan 2026 22:16:49 +0530 Subject: [PATCH 21/49] "url" capitalized as "URL" "url" capitalized as "URL" Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/powershell/tests/Test-Assessment.25408.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/powershell/tests/Test-Assessment.25408.md b/src/powershell/tests/Test-Assessment.25408.md index 2db8c5815..65eaa527b 100644 --- a/src/powershell/tests/Test-Assessment.25408.md +++ b/src/powershell/tests/Test-Assessment.25408.md @@ -1,4 +1,4 @@ -Web Content Filtering in Microsoft Entra Internet Access helps organizations control access to websites based on its web categories, domains or url, reducing exposure to malicious or inappropriate content. When traffic is routed through Microsoft Entra Internet Access, filtering policies can block or allow entire categories like Gambling or Social Media, or specific domains/url, ensuring safer browsing across all devices and locations. +Web Content Filtering in Microsoft Entra Internet Access helps organizations control access to websites based on its web categories, domains or URL, reducing exposure to malicious or inappropriate content. When traffic is routed through Microsoft Entra Internet Access, filtering policies can block or allow entire categories like Gambling or Social Media, or specific domains/URL, ensuring safer browsing across all devices and locations. Configuring these policies is critical for security and compliance. It prevents phishing and malware risks, enforces corporate standards, and improves productivity by restricting non-business sites. Combined with identity-aware Conditional Access, Web Content Filtering delivers dynamic, cloud-based protection that aligns with modern Zero Trust principles. From 802dfa63155eb5227a375f87ca853e5fab360292 Mon Sep 17 00:00:00 2001 From: Sandeep Jha Date: Wed, 7 Jan 2026 22:22:46 +0530 Subject: [PATCH 22/49] corrected grammar corrected grammar --- src/powershell/tests/Test-Assessment.25408.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/powershell/tests/Test-Assessment.25408.md b/src/powershell/tests/Test-Assessment.25408.md index 65eaa527b..7be60088e 100644 --- a/src/powershell/tests/Test-Assessment.25408.md +++ b/src/powershell/tests/Test-Assessment.25408.md @@ -1,4 +1,4 @@ -Web Content Filtering in Microsoft Entra Internet Access helps organizations control access to websites based on its web categories, domains or URL, reducing exposure to malicious or inappropriate content. When traffic is routed through Microsoft Entra Internet Access, filtering policies can block or allow entire categories like Gambling or Social Media, or specific domains/URL, ensuring safer browsing across all devices and locations. +Web Content Filtering in Microsoft Entra Internet Access helps organizations control access to websites based on web categories, domains or URL, reducing exposure to malicious or inappropriate content. When traffic is routed through Microsoft Entra Internet Access, filtering policies can block or allow entire categories like Gambling or Social Media, or specific domains/URL, ensuring safer browsing across all devices and locations. Configuring these policies is critical for security and compliance. It prevents phishing and malware risks, enforces corporate standards, and improves productivity by restricting non-business sites. Combined with identity-aware Conditional Access, Web Content Filtering delivers dynamic, cloud-based protection that aligns with modern Zero Trust principles. From 1ea5d16b0a73a02c122cb9fe9e08389792d8c075 Mon Sep 17 00:00:00 2001 From: Sandeep Jha Date: Thu, 8 Jan 2026 03:11:46 +0530 Subject: [PATCH 23/49] simplified priority check --- src/powershell/tests/Test-Assessment.25408.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/powershell/tests/Test-Assessment.25408.ps1 b/src/powershell/tests/Test-Assessment.25408.ps1 index 597d58275..d2e268989 100644 --- a/src/powershell/tests/Test-Assessment.25408.ps1 +++ b/src/powershell/tests/Test-Assessment.25408.ps1 @@ -84,7 +84,8 @@ function Test-Assessment-25408 { if ($policyLink) { # Determine profile type based on priority - $profileType = if ($null -ne $filteringProfile.priority -and $filteringProfile.priority -eq 65000) { 'Baseline Profile' } elseif ($null -ne $filteringProfile.priority -and $filteringProfile.priority -lt 65000) { 'Security Profile' } + $priority = $filteringProfile.priority + $profileType = if ($priority -eq 65000) { 'Baseline Profile' } elseif ($null -ne $priority -and $priority -lt 65000) { 'Security Profile' } $profileInfo = [PSCustomObject]@{ ProfileId = $filteringProfile.id From da4d1fd6044c0e57cd2ea36e65c1d8cb590beda7 Mon Sep 17 00:00:00 2001 From: aahmed-spec Date: Thu, 8 Jan 2026 10:12:14 +0530 Subject: [PATCH 24/49] Added comments for Test-IsBroadCidr function Clarified output descriptions in Test-IsBroadCidr function. --- src/powershell/tests/Test-Assessment.25395.ps1 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/powershell/tests/Test-Assessment.25395.ps1 b/src/powershell/tests/Test-Assessment.25395.ps1 index 8410f2363..43c416fdc 100644 --- a/src/powershell/tests/Test-Assessment.25395.ps1 +++ b/src/powershell/tests/Test-Assessment.25395.ps1 @@ -40,8 +40,13 @@ function Test-Assessment-25395 { <# .SYNOPSIS Checks if a CIDR range is overly permissive (/16 or broader). + .DESCRIPTION + CIDR ranges with prefix length <= 16 are treated as overly permissive. + This includes /16 itself (65,536 IPs) and any broader ranges such as /15, /14, etc. .OUTPUTS - System.Boolean - True if CIDR is /16 or broader, false otherwise. + System.Boolean + True - CIDR prefix length <= 16 + False - CIDR prefix length > 16 or invalid format #> param([string]$Cidr) if ($Cidr -match '/(\d+)$') { return ([int]$matches[1] -le 16) } From cc80027d542f8b6b7f41498c75f97d7e8c878d8a Mon Sep 17 00:00:00 2001 From: aahmed-spec Date: Thu, 8 Jan 2026 10:15:37 +0530 Subject: [PATCH 25/49] Added BroadPortRangeThreshold variable for src/powershell/tests/Test-Assessment.25395.ps1 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/powershell/tests/Test-Assessment.25395.ps1 | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/powershell/tests/Test-Assessment.25395.ps1 b/src/powershell/tests/Test-Assessment.25395.ps1 index 43c416fdc..65cc072f9 100644 --- a/src/powershell/tests/Test-Assessment.25395.ps1 +++ b/src/powershell/tests/Test-Assessment.25395.ps1 @@ -79,8 +79,12 @@ function Test-Assessment-25395 { System.Boolean - True if port range is considered too broad, false otherwise. #> param([string]$Port) + + # Maximum number of ports allowed in a range before it is considered "broad". + $BroadPortRangeThreshold = 10 + if ($Port -eq '1-65535') { return $true } - if ($Port -match '^(\d+)-(\d+)$' -and (([int]$matches[2] - [int]$matches[1]) -gt 10)) { return $true } + if ($Port -match '^(\d+)-(\d+)$' -and (([int]$matches[2] - [int]$matches[1]) -gt $BroadPortRangeThreshold)) { return $true } return $false } From 6244d6ba2d5e41cef700ebc87f31222dd272937a Mon Sep 17 00:00:00 2001 From: aahmed-spec Date: Thu, 8 Jan 2026 10:55:44 +0530 Subject: [PATCH 26/49] Update line no: 69 src/powershell/tests/Test-Assessment.25395.ps1 updated 255 instead of 256 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/powershell/tests/Test-Assessment.25395.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/powershell/tests/Test-Assessment.25395.ps1 b/src/powershell/tests/Test-Assessment.25395.ps1 index 65cc072f9..8c3210a08 100644 --- a/src/powershell/tests/Test-Assessment.25395.ps1 +++ b/src/powershell/tests/Test-Assessment.25395.ps1 @@ -66,7 +66,7 @@ function Test-Assessment-25395 { $end = [System.Net.IPAddress]::Parse($matches[2]).GetAddressBytes() [array]::Reverse($start) [array]::Reverse($end) - return (([BitConverter]::ToUInt32($end,0) - [BitConverter]::ToUInt32($start,0)) -gt 256) + return (([BitConverter]::ToUInt32($end,0) - [BitConverter]::ToUInt32($start,0)) -gt 255) } return $false } From 0349d5aac13449e669de4e1d88feff28d2264027 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 05:34:05 +0000 Subject: [PATCH 27/49] Initial plan From 6b15741379a71e3acc7d921a1824c251426a2066 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 05:36:37 +0000 Subject: [PATCH 28/49] Fix off-by-one errors in IP and port range calculations Co-authored-by: aahmed-spec <250927798+aahmed-spec@users.noreply.github.com> --- src/powershell/tests/Test-Assessment.25395.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25395.ps1 b/src/powershell/tests/Test-Assessment.25395.ps1 index 8c3210a08..cd5575438 100644 --- a/src/powershell/tests/Test-Assessment.25395.ps1 +++ b/src/powershell/tests/Test-Assessment.25395.ps1 @@ -66,7 +66,7 @@ function Test-Assessment-25395 { $end = [System.Net.IPAddress]::Parse($matches[2]).GetAddressBytes() [array]::Reverse($start) [array]::Reverse($end) - return (([BitConverter]::ToUInt32($end,0) - [BitConverter]::ToUInt32($start,0)) -gt 255) + return (([BitConverter]::ToUInt32($end,0) - [BitConverter]::ToUInt32($start,0) + 1) -gt 256) } return $false } @@ -84,7 +84,7 @@ function Test-Assessment-25395 { $BroadPortRangeThreshold = 10 if ($Port -eq '1-65535') { return $true } - if ($Port -match '^(\d+)-(\d+)$' -and (([int]$matches[2] - [int]$matches[1]) -gt $BroadPortRangeThreshold)) { return $true } + if ($Port -match '^(\d+)-(\d+)$' -and (([int]$matches[2] - [int]$matches[1] + 1) -gt $BroadPortRangeThreshold)) { return $true } return $false } From c160132cc7739148bd306e5828ae525790b1d0d3 Mon Sep 17 00:00:00 2001 From: Komal Date: Thu, 8 Jan 2026 12:10:49 +0530 Subject: [PATCH 29/49] fix invetigate status and md file --- src/powershell/tests/Test-Assessment.35011.md | 16 ++++--- .../tests/Test-Assessment.35011.ps1 | 44 +++++++++++-------- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.35011.md b/src/powershell/tests/Test-Assessment.35011.md index 92b9d8f5a..614ac023d 100644 --- a/src/powershell/tests/Test-Assessment.35011.md +++ b/src/powershell/tests/Test-Assessment.35011.md @@ -6,12 +6,12 @@ Without super user configuration, organizations risk data loss when encryption k To configure super users: -Connect to Azure Information Protection PowerShell: Connect-AipService -Enable the super user feature: Enable-AipServiceSuperUserFeature -Add super users (service accounts recommended): -For user accounts: Add-AipServiceSuperUser -EmailAddress "serviceaccount@contoso.com" -For service principals: Add-AipServiceSuperUser -ServicePrincipalId "service-principal-id" -Verify configuration: Get-AipServiceSuperUser +1. Connect to Azure Information Protection PowerShell: `Connect-AipService` +2. Enable the super user feature: `Enable-AipServiceSuperUserFeature` +3. Add super users (service accounts recommended): + - For user accounts: `Add-AipServiceSuperUser -EmailAddress "serviceaccount@contoso.com"` + - For service principals: `Add-AipServiceSuperUser -ServicePrincipalId "service-principal-id"` +4. Verify configuration: `Get-AipServiceSuperUser` Best practices: @@ -21,7 +21,9 @@ Best practices: - Audit super user access regularly - Document business justification for each super user account -[Configure super users for Azure Information Protection Enable-AipServiceSuperUserFeature Add-AipServiceSuperUser](https://learn.microsoft.com/en-us/powershell/module/aipservice/enable-aipservicesuperuserfeature) +- [Configure super users for Azure Information Protection](https://learn.microsoft.com/en-us/purview/encryption-super-users) +- [Enable-AipServiceSuperUserFeature](https://learn.microsoft.com/en-us/powershell/module/aipservice/enable-aipservicesuperuserfeature) +- [Add-AipServiceSuperUser](https://learn.microsoft.com/en-us/powershell/module/aipservice/add-aipservicesuperuser) %TestResult% diff --git a/src/powershell/tests/Test-Assessment.35011.ps1 b/src/powershell/tests/Test-Assessment.35011.ps1 index c6a5591ea..28c397c26 100644 --- a/src/powershell/tests/Test-Assessment.35011.ps1 +++ b/src/powershell/tests/Test-Assessment.35011.ps1 @@ -84,15 +84,10 @@ function Test-Assessment-35011 { #region Report Generation $testResultMarkdown = "" + $mdInfo = "" if ($investigateFlag) { $testResultMarkdown = "⚠️ Unable to determine AIP super user configuration due to permissions or connection issues.`n`n" - $testResultMarkdown += "**Error Details:**`n" - $testResultMarkdown += "* $errorMsg`n`n" - $testResultMarkdown += "**Possible Causes:**`n" - $testResultMarkdown += "* AipService module not installed (requires v3.0+)`n" - $testResultMarkdown += "* Not connected to AIP service`n" - $testResultMarkdown += "* Insufficient permissions to query AIP configuration`n" } else { if ($passed) { @@ -107,38 +102,51 @@ function Test-Assessment-35011 { } } - $testResultMarkdown += "### Azure Information Protection Super User Configuration`n`n" - $testResultMarkdown += "**Feature Status:**`n" + # Build detailed information section + $mdInfo = "## Azure Information Protection Super User Configuration`n`n" $featureStatus = if ($superUserFeatureEnabled) { "Enabled" } else { "Disabled" } - $testResultMarkdown += "* Super User Feature: $featureStatus`n`n" + $mdInfo += "**Super User Feature: $featureStatus**`n`n" if ($superUserFeatureEnabled) { $superUserCount = if ($superUsers) { @($superUsers).Count } else { 0 } - $testResultMarkdown += "**Super Users Configured: $superUserCount**`n`n" + $mdInfo += "**Super Users Configured: $superUserCount**`n`n" if ($superUserCount -gt 0) { - $testResultMarkdown += "| Email Address / Service Principal ID | Account Type |`n" - $testResultMarkdown += "| :--- | :--- |`n" + $mdInfo += "| Email Address / Service Principal ID | Account Type |`n" + $mdInfo += "| :--- | :--- |`n" foreach ($superUser in $superUsers) { $accountType = if ($superUser -like '*-*-*-*-*') { "Service Principal" } else { "User" } - $testResultMarkdown += "| $superUser | $accountType |`n" + $mdInfo += "| $superUser | $accountType |`n" } - $testResultMarkdown += "`n" + $mdInfo += "`n" } } - $testResultMarkdown += "**Note:** Super user configuration is not available through the Azure portal and must be managed via PowerShell using the AipService module.`n" + $mdInfo += "**Note:** Super user configuration is not available through the Azure portal and must be managed via PowerShell using the AipService module.`n" + + # Add mdInfo to the main markdown if there's content + if ($mdInfo) { + $testResultMarkdown += "%TestResult%" + } } #endregion Report Generation - $testResultDetail = @{ + # Replace placeholder with actual detailed info + if ($mdInfo) { + $testResultMarkdown = $testResultMarkdown -replace "%TestResult%", $mdInfo + } + + $params = @{ TestId = '35011' - Title = 'Azure Information Protection (AIP) Super User Feature' Status = $passed Result = $testResultMarkdown } - Add-ZtTestResultDetail @testResultDetail + # Add investigate status if needed + if ($investigateFlag -eq $true) { + $params.CustomStatus = 'Investigate' + } + Add-ZtTestResultDetail @params } From 184a79c5f6472b6cfb53cede4490230d9bbbcbfe Mon Sep 17 00:00:00 2001 From: Afif Ahmed Patel Date: Thu, 8 Jan 2026 12:43:23 +0530 Subject: [PATCH 30/49] Update test logic for assessment 25395 --- .../tests/Test-Assessment.25395.ps1 | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/powershell/tests/Test-Assessment.25395.ps1 b/src/powershell/tests/Test-Assessment.25395.ps1 index cd5575438..7f59076af 100644 --- a/src/powershell/tests/Test-Assessment.25395.ps1 +++ b/src/powershell/tests/Test-Assessment.25395.ps1 @@ -116,6 +116,29 @@ function Test-Assessment-25395 { return ($AD_WELL_KNOWN_PORTS -contains $Port) } + function Test-ContainsAdWellKnownPort { + <# + .SYNOPSIS + Checks if a port range contains any well-known Active Directory ports. + .DESCRIPTION + Evaluates whether a port range (e.g., '50-500') includes any of the + well-known AD ports (53, 88, 135, 389, 445, 464, 636, 3268, 3269). + .OUTPUTS + System.Boolean - True if range contains AD ports, false otherwise. + #> + param([string]$Port) + if ($Port -match '^(\d+)-(\d+)$') { + $start = [int]$matches[1] + $end = [int]$matches[2] + foreach ($adPort in $AD_WELL_KNOWN_PORTS) { + if ([int]$adPort -ge $start -and [int]$adPort -le $end) { + return $true + } + } + } + return $false + } + #endregion Helper Functions #region Data Collection @@ -208,10 +231,16 @@ function Test-Assessment-25395 { # Step 3: Evaluate port breadth with AD RPC exceptions foreach ($port in $segment.ports) { if (Test-IsBroadPortRange $port) { + # Check if this is a valid AD RPC exception or exact AD well-known port if (-not (Test-IsAdRpcException -AppName $app.displayName -Port $port) ` -and -not (Test-IsAdWellKnownPort $port)) { $hasBroadPorts = $true $issues += 'Broad port range' + + # Additionally flag if the broad range contains AD well-known ports + if (Test-ContainsAdWellKnownPort $port) { + $issues += 'Broad range overlaps AD ports' + } } } } @@ -356,7 +385,6 @@ function Test-Assessment-25395 { # Replace the placeholder with detailed information $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo #endregion Report Generation - $params = @{ TestId = '25395' Title = 'Private Access application segments enforce least-privilege access' From c1fdf84fa0e6cfedc948052b8a78d0e69483544b Mon Sep 17 00:00:00 2001 From: Afif Ahmed Patel Date: Thu, 8 Jan 2026 13:35:30 +0530 Subject: [PATCH 31/49] Refine segment evaluation logic for assessment 25395 --- src/powershell/tests/Test-Assessment.25395.ps1 | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25395.ps1 b/src/powershell/tests/Test-Assessment.25395.ps1 index 7f59076af..9753b8e2b 100644 --- a/src/powershell/tests/Test-Assessment.25395.ps1 +++ b/src/powershell/tests/Test-Assessment.25395.ps1 @@ -254,6 +254,7 @@ function Test-Assessment-25395 { if ($issues.Count -gt 0) { $segmentFindings += [PSCustomObject]@{ AppName = $app.displayName + AppId = $app.appId SegmentId = $segment.id Issue = ($issues -join ', ') Destination = $segment.destinationHost @@ -369,7 +370,7 @@ function Test-Assessment-25395 { if ($segmentFindings.Count -gt 0) { $tableRows = "" $formatTemplate = @' -## Segment Findings +## Segment findings | App name | Segment id | Issue | Destination | Ports | Recommendation | |---|---|---|---|---|---| @@ -377,7 +378,9 @@ function Test-Assessment-25395 { '@ foreach ($f in $segmentFindings) { - $tableRows += "| $(Get-SafeMarkdown $f.AppName) | $($f.SegmentId) | $($f.Issue) | $($f.Destination) | $($f.Ports) | Narrow destination and ports |`n" + $appLink = "https://entra.microsoft.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/overview/appId/$($f.AppId)" + $linkedAppName = "[{0}]({1})" -f (Get-SafeMarkdown $f.AppName), $appLink + $tableRows += "| $linkedAppName | $($f.SegmentId) | $($f.Issue) | $($f.Destination) | $($f.Ports) | Narrow destination and ports |`n" } $mdInfo += $formatTemplate -f $tableRows } From 0cfa271593f3f50a47983c8f3b4db802b02e87b7 Mon Sep 17 00:00:00 2001 From: Ashwini Karke Date: Thu, 8 Jan 2026 15:05:36 +0530 Subject: [PATCH 32/49] updated test --- src/powershell/tests/Test-Assessment.35013.md | 28 ---- .../tests/Test-Assessment.35013.ps1 | 153 ------------------ src/powershell/tests/Test-Assessment.35025.md | 23 +++ .../tests/Test-Assessment.35025.ps1 | 92 ++++------- 4 files changed, 53 insertions(+), 243 deletions(-) delete mode 100644 src/powershell/tests/Test-Assessment.35013.md delete mode 100644 src/powershell/tests/Test-Assessment.35013.ps1 create mode 100644 src/powershell/tests/Test-Assessment.35025.md diff --git a/src/powershell/tests/Test-Assessment.35013.md b/src/powershell/tests/Test-Assessment.35013.md deleted file mode 100644 index cacf61e61..000000000 --- a/src/powershell/tests/Test-Assessment.35013.md +++ /dev/null @@ -1,28 +0,0 @@ -# Encryption-Enabled Sensitivity Labels - -## Description - -Sensitivity labels provide classification capabilities, but without encryption, labels merely mark content as sensitive without preventing unauthorized access. Encryption-enabled labels apply Azure Rights Management protection to documents and emails, enforcing access controls that persist with the content regardless of where it is stored or shared. - -Users with "Confidential" labels on documents can still forward those files to unauthorized recipients unless encryption prevents file access based on identity. Organizations investing in sensitivity label frameworks without enabling encryption gain visibility into data classification but lack technical enforcement of protection policies. - -Encrypted labels ensure that only authorized users and applications can decrypt content, preventing data exfiltration even if files are leaked, stolen, or improperly shared. At least one encryption-enabled label should exist for high-value data requiring protection beyond classification metadata. - -## How to fix - -To enable encryption on sensitivity labels: - -1. Navigate to **Microsoft Purview portal** → **Information protection** → **Labels** -2. Create a new label or edit an existing label -3. Under label scope, ensure **Items** is selected -4. In protection settings, enable **Apply or remove encryption** -5. Configure encryption settings: - - **Assign permissions now** (define specific users/groups) - - **Let users assign permissions** (Do Not Forward or user-defined) -6. Save and publish the label through a label policy - -## Learn more - -- [Restrict access to content by using encryption in sensitivity labels](https://learn.microsoft.com/en-us/microsoft-365/compliance/encryption-sensitivity-labels) -- [Apply encryption using sensitivity labels](https://learn.microsoft.com/en-us/purview/sensitivity-labels-office-apps) -- [Information protection in Microsoft 365](https://learn.microsoft.com/en-us/microsoft-365/compliance/information-protection) diff --git a/src/powershell/tests/Test-Assessment.35013.ps1 b/src/powershell/tests/Test-Assessment.35013.ps1 deleted file mode 100644 index e045ab3fa..000000000 --- a/src/powershell/tests/Test-Assessment.35013.ps1 +++ /dev/null @@ -1,153 +0,0 @@ -<# -.SYNOPSIS - Validates that at least one encryption-enabled sensitivity label is configured. - -.DESCRIPTION - This test checks if at least one sensitivity label has encryption enabled. Encryption-enabled labels - apply Azure Rights Management protection to documents and emails, enforcing access controls that - persist with the content regardless of where it is stored or shared. - -.NOTES - Test ID: 35013 - Category: Data - Pillar: Data - Required Module: ExchangeOnlineManagement - Required Connection: Exchange Online (Security & Compliance Center) -#> - -function Test-Assessment-35013 { - [ZtTest( - Category = 'Data', - ImplementationCost = 'Medium', - MinimumLicense = 'Microsoft_365_E3', - Pillar = 'Data', - RiskLevel = 'High', - SfiPillar = 'Protect tenants and production systems', - TenantType = ('Workforce', 'External'), - TestId = 35013, - Title = 'Encryption-enabled sensitivity labels are configured', - UserImpact = 'High' - )] - [CmdletBinding()] - param() - - #region Data Collection - Write-PSFMessage 'Start Encryption-Enabled Labels evaluation' -Tag Test -Level VeryVerbose - - $activity = 'Checking Encryption-Enabled Labels' - Write-ZtProgress -Activity $activity -Status 'Getting sensitivity labels' - - # Q1: Retrieve all sensitivity labels - try { - $labels = Get-Label -ErrorAction Stop - } - catch { - Write-PSFMessage "Failed to retrieve sensitivity labels: $_" -Tag Test -Level Warning - - $params = @{ - TestId = '35013' - Title = 'Encryption-enabled sensitivity labels are configured' - Status = $false - Result = "❌ Unable to retrieve sensitivity labels. Ensure you are connected to Exchange Online Security & Compliance Center.`n`nError: $_" - } - Add-ZtTestResultDetail @params - return - } - - # Q2: Filter for encryption-enabled labels - $encryptionEnabledLabels = $labels | Where-Object { $_.EncryptionEnabled -eq $true } - - # Q3: Check if any labels have null/undefined EncryptionEnabled property - $undeterminedLabels = $labels | Where-Object { $null -eq $_.EncryptionEnabled } - #endregion Data Collection - - #region Assessment Logic - $passed = $false - $customStatus = $null - - # Investigate: Cannot determine encryption configuration - if ($undeterminedLabels.Count -gt 0 -and $encryptionEnabledLabels.Count -eq 0) { - $passed = $true - $customStatus = 'Investigate' - } - # Pass: At least one encryption-enabled label exists - elseif ($encryptionEnabledLabels.Count -ge 1) { - $passed = $true - } - # Fail: No encryption-enabled labels exist - else { - $passed = $false - } - #endregion Assessment Logic - - #region Report Generation - $testResultMarkdown = '' - - if ($customStatus -eq 'Investigate') { - $testResultMarkdown = "⚠️ Labels exist but encryption configuration cannot be determined`n`n" - } - elseif ($passed) { - $testResultMarkdown = "✅ At least one encryption-enabled sensitivity label is configured`n`n" - } - else { - $testResultMarkdown = "❌ No encryption-enabled labels exist; all labels provide classification only`n`n" - } - - # Build detailed information - $mdInfo = "## Summary`n`n" - $mdInfo += "- **Total Sensitivity Labels**: $($labels.Count)`n" - $mdInfo += "- **Encryption-Enabled Labels**: $($encryptionEnabledLabels.Count)`n" - - if ($undeterminedLabels.Count -gt 0) { - $mdInfo += "- **Labels with Undetermined Encryption**: $($undeterminedLabels.Count)`n" - } - - $mdInfo += "`n" - - if ($encryptionEnabledLabels.Count -gt 0) { - $mdInfo += "## Encryption-Enabled Labels`n`n" - $mdInfo += "| Label name | Enabled | Content type |`n" - $mdInfo += "| :--------- | :------ | :----------- |`n" - - foreach ($label in ($encryptionEnabledLabels | Sort-Object DisplayName)) { - $enabledStatus = if ($label.Disabled -eq $false) { '✅ Yes' } else { '❌ No' } - $contentType = if ($label.ContentType) { $label.ContentType -join ', ' } else { 'All' } - - $mdInfo += "| $(Get-SafeMarkdown $label.DisplayName) | $enabledStatus | $contentType |`n" - } - $mdInfo += "`n" - } - - if ($undeterminedLabels.Count -gt 0) { - $mdInfo += "## Labels with Undetermined Encryption Configuration`n`n" - $mdInfo += "| Label name | Enabled | Content type |`n" - $mdInfo += "| :--------- | :------ | :----------- |`n" - - foreach ($label in ($undeterminedLabels | Sort-Object DisplayName)) { - $enabledStatus = if ($label.Disabled -eq $false) { '✅ Yes' } else { '❌ No' } - $contentType = if ($label.ContentType) { $label.ContentType -join ', ' } else { 'All' } - - $mdInfo += "| $(Get-SafeMarkdown $label.DisplayName) | $enabledStatus | $contentType |`n" - } - $mdInfo += "`n⚠️ These labels have null or undefined EncryptionEnabled property. Verify encryption configuration manually.`n`n" - } - - if ($encryptionEnabledLabels.Count -eq 0 -and $undeterminedLabels.Count -eq 0) { - $mdInfo += "⚠️ **Recommendation**: Create at least one sensitivity label with encryption enabled to protect high-value data.`n`n" - $mdInfo += "Encryption ensures that only authorized users and applications can decrypt content, preventing data exfiltration even if files are leaked, stolen, or improperly shared.`n" - } - - $testResultMarkdown += $mdInfo - #endregion Report Generation - - $params = @{ - TestId = '35013' - Title = 'Encryption-enabled sensitivity labels are configured' - Status = $passed - Result = $testResultMarkdown - } - if ($customStatus) { - $params.CustomStatus = $customStatus - } - Add-ZtTestResultDetail @params -} diff --git a/src/powershell/tests/Test-Assessment.35025.md b/src/powershell/tests/Test-Assessment.35025.md new file mode 100644 index 000000000..f8ba02368 --- /dev/null +++ b/src/powershell/tests/Test-Assessment.35025.md @@ -0,0 +1,23 @@ +Azure RMS includes both external and internal licensing capabilities that must be configured separately. While Azure RMS activation (test 35024) enables the service globally, internal RMS licensing specifically allows users and services within the organization to license protected content for internal distribution and sharing. Without internal RMS licensing enabled, users cannot share rights-protected content with internal recipients, preventing collaboration on encrypted emails and files within the organization. Internal RMS licensing must be explicitly enabled alongside super user configuration to ensure that legal holds, eDiscovery, and data recovery operations can access encrypted content. Organizations that have enabled Azure RMS but not internal licensing inadvertently block internal protected content sharing while potentially leaving external sharing unprotected. Both internal and external RMS licensing settings should be configured together as part of a comprehensive rights management strategy. + +**Remediation action** + +To enable internal RMS licensing: + +1. Verify Azure RMS is enabled (test 35024) - internal licensing requires Azure RMS to be active +2. Sign in as Global Administrator to the [Microsoft Purview portal](https://purview.microsoft.com) +3. Navigate to Settings > Encryption > Azure Information Protection +4. Review RMS licensing configuration settings +5. Ensure internal licensing and distribution settings are enabled for the organization +6. If not enabled, contact Microsoft Support to activate internal licensing configuration + +For organizations using Exchange Online, ensure mail flow policies and RMS features are not blocked: +1. Connect to Exchange Online: `Connect-ExchangeOnline` +2. Verify internal licensing is enabled: `Set-IRMConfiguration -InternalLicensingEnabled $true` +3. Verify the setting: `Get-IRMConfiguration | Select-Object -Property InternalLicensingEnabled, ExternalLicensingEnabled` + +- [Configure Azure Rights Management licensing](https://learn.microsoft.com/en-us/purview/encryption-configure-aip) +- [Rights Management in Exchange Online](https://learn.microsoft.com/en-us/purview/encryption-exchange-owa) + + +%TestResult% diff --git a/src/powershell/tests/Test-Assessment.35025.ps1 b/src/powershell/tests/Test-Assessment.35025.ps1 index 17ac016a6..43db429fc 100644 --- a/src/powershell/tests/Test-Assessment.35025.ps1 +++ b/src/powershell/tests/Test-Assessment.35025.ps1 @@ -9,7 +9,7 @@ .NOTES Test ID: 35025 - Category: Data + Category: Rights Management Service (RMS) Pillar: Data Required Module: ExchangeOnlineManagement Required Connection: Exchange Online @@ -17,65 +17,59 @@ function Test-Assessment-35025 { [ZtTest( - Category = 'Data', + Category = 'Rights Management Service (RMS)', ImplementationCost = 'Low', - MinimumLicense = 'Microsoft_365_E3', + MinimumLicense = ('Microsoft 365 E3'), Pillar = 'Data', RiskLevel = 'High', SfiPillar = 'Protect tenants and production systems', - TenantType = ('Workforce', 'External'), + TenantType = ('Workforce'), TestId = 35025, - Title = 'Internal RMS licensing is enabled', + Title = 'Internal RMS Licensing Enabled', UserImpact = 'High' )] [CmdletBinding()] param() #region Data Collection - Write-PSFMessage 'Start Internal RMS Licensing evaluation' -Tag Test -Level VeryVerbose + Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose - $activity = 'Checking Internal RMS Licensing' + $activity = 'Checking Internal RMS Licensing Status' Write-ZtProgress -Activity $activity -Status 'Getting IRM configuration' # Q1: Get IRM licensing configuration + $irmConfig = $null + $errorMsg = $null + try { $irmConfig = Get-IRMConfiguration -ErrorAction Stop } catch { + $errorMsg = $_ Write-PSFMessage "Failed to retrieve IRM configuration: $_" -Tag Test -Level Warning - - $params = @{ - TestId = '35025' - Title = 'Internal RMS licensing is enabled' - Status = $false - Result = "❌ Unable to retrieve IRM configuration. Ensure you are connected to Exchange Online with Connect-ExchangeOnline.`n`nError: $_" - } - Add-ZtTestResultDetail @params - return } - - # Q2: Check if internal licensing is enabled - $internalLicensingEnabled = $irmConfig.InternalLicensingEnabled - - # Q3: Get detailed licensing configuration - $detailedConfig = $irmConfig | Select-Object -Property InternalLicensingEnabled, ExternalLicensingEnabled, AzureRMSLicensingEnabled #endregion Data Collection #region Assessment Logic $passed = $false $customStatus = $null - # Investigate: Cannot determine licensing status - if ($null -eq $internalLicensingEnabled) { - $passed = $true + if ($errorMsg) { + # Investigate: Cannot query IRM configuration + $passed = $false $customStatus = 'Investigate' } - # Pass: Internal RMS licensing is enabled - elseif ($internalLicensingEnabled -eq $true) { + elseif ($null -eq $irmConfig.InternalLicensingEnabled) { + # Investigate: Cannot determine licensing status + $passed = $false + $customStatus = 'Investigate' + } + elseif ($irmConfig.InternalLicensingEnabled -eq $true) { + # Pass: Internal RMS licensing is enabled $passed = $true } - # Fail: Internal RMS licensing is not enabled else { + # Fail: Internal RMS licensing is not enabled $passed = $false } #endregion Assessment Logic @@ -85,16 +79,6 @@ function Test-Assessment-35025 { if ($customStatus -eq 'Investigate') { $testResultMarkdown = "⚠️ Unable to determine internal RMS licensing status due to permissions issues or incomplete configuration data.`n`n" - - $params = @{ - TestId = '35025' - Title = 'Internal RMS licensing is enabled' - Status = $passed - Result = $testResultMarkdown - CustomStatus = $customStatus - } - Add-ZtTestResultDetail @params - return } elseif ($passed) { $testResultMarkdown = "✅ Internal RMS licensing is enabled, allowing internal users to license and share protected content within the organization.`n`n" @@ -103,36 +87,20 @@ function Test-Assessment-35025 { $testResultMarkdown = "❌ Internal RMS licensing is not enabled or licensing endpoints are not configured.`n`n" } - # Build detailed information - $mdInfo = "## [Internal RMS Licensing Configuration](https://purview.microsoft.com/settings/encryption)`n`n" - $mdInfo += "| Setting | Status |`n" - $mdInfo += "| :------ | :----- |`n" - $mdInfo += "| Internal Licensing Enabled | $(if ($detailedConfig.InternalLicensingEnabled -eq $true) { '✅ Enabled' } elseif ($detailedConfig.InternalLicensingEnabled -eq $false) { '❌ Disabled' } else { '⚠️ Unknown' }) |`n" - $mdInfo += "| External Licensing Enabled | $(if ($detailedConfig.ExternalLicensingEnabled -eq $true) { '✅ Enabled' } elseif ($detailedConfig.ExternalLicensingEnabled -eq $false) { '❌ Disabled' } else { '⚠️ Unknown' }) |`n" - $mdInfo += "| Azure RMS Licensing Enabled | $(if ($detailedConfig.AzureRMSLicensingEnabled -eq $true) { '✅ Enabled' } elseif ($detailedConfig.AzureRMSLicensingEnabled -eq $false) { '❌ Disabled' } else { '⚠️ Unknown' }) |`n" - $mdInfo += "`n" - - # Additional configuration details + # Build detailed information table if we have data if ($irmConfig) { - $mdInfo += "## Additional Configuration Details`n`n" - - if ($irmConfig.LicensingLocation) { - $mdInfo += "- **Licensing Location**: $(Get-SafeMarkdown $irmConfig.LicensingLocation)`n" - } - - if ($irmConfig.RMSOnlineKeySharingLocation) { - $mdInfo += "- **RMS Online Key Sharing Location**: $(Get-SafeMarkdown $irmConfig.RMSOnlineKeySharingLocation)`n" - } - - $mdInfo += "`n" + $testResultMarkdown += "## [Internal RMS licensing configuration](https://purview.microsoft.com/settings/encryption)`n`n" + $testResultMarkdown += "| Setting | Status |`n" + $testResultMarkdown += "| :--- | :--- |`n" + $testResultMarkdown += "| Internal licensing enabled | $(if ($irmConfig.InternalLicensingEnabled -eq $true) { '✅ enabled' } elseif ($irmConfig.InternalLicensingEnabled -eq $false) { '❌ disabled' } else { '⚠️ unknown' }) |`n" + $testResultMarkdown += "| External licensing enabled | $(if ($irmConfig.ExternalLicensingEnabled -eq $true) { '✅ enabled' } elseif ($irmConfig.ExternalLicensingEnabled -eq $false) { '❌ disabled' } else { '⚠️ unknown' }) |`n" + $testResultMarkdown += "| Azure RMS licensing enabled | $(if ($irmConfig.AzureRMSLicensingEnabled -eq $true) { '✅ enabled' } elseif ($irmConfig.AzureRMSLicensingEnabled -eq $false) { '❌ disabled' } else { '⚠️ unknown' }) |`n" } - - $testResultMarkdown += $mdInfo #endregion Report Generation $params = @{ TestId = '35025' - Title = 'Internal RMS licensing is enabled' + Title = 'Internal RMS Licensing Enabled' Status = $passed Result = $testResultMarkdown } From 8c9c78d6defcdc9969fc3b610791fa4b13846ef2 Mon Sep 17 00:00:00 2001 From: Komal Date: Thu, 8 Jan 2026 15:49:33 +0530 Subject: [PATCH 33/49] add test 35030 --- src/powershell/tests/Test-Assessment.35030.md | 34 +++++ .../tests/Test-Assessment.35030.ps1 | 123 ++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 src/powershell/tests/Test-Assessment.35030.md create mode 100644 src/powershell/tests/Test-Assessment.35030.ps1 diff --git a/src/powershell/tests/Test-Assessment.35030.md b/src/powershell/tests/Test-Assessment.35030.md new file mode 100644 index 000000000..0cdb339d7 --- /dev/null +++ b/src/powershell/tests/Test-Assessment.35030.md @@ -0,0 +1,34 @@ +Data Loss Prevention (DLP) policies protect sensitive information by monitoring, detecting, and preventing the sharing of confidential data across Microsoft 365 workloads including Exchange Online, SharePoint Online, OneDrive, and Microsoft Teams. + +When DLP policies are not enabled or configured, organizations lack automated controls to prevent accidental or intentional disclosure of sensitive information such as credit card numbers, social security numbers, financial data, or proprietary information. Without active DLP policies, employees can freely share sensitive content through email, file uploads, or team communications without organizational oversight, increasing the risk of data breaches, regulatory violations (GDPR, HIPAA, PCI-DSS), and reputational damage. + +Enabling and configuring at least one DLP policy ensures organizations have automated detection and response capabilities for sensitive data, reducing the risk of unauthorized data exfiltration and demonstrating compliance readiness to regulators and auditors. + +**Remediation action** + +To create and enable DLP policies: + +1. Sign in as a Global Administrator or Compliance Administrator to the [Microsoft Purview portal](https://purview.microsoft.com) +2. Navigate to Data Loss Prevention > Policies +3. Select "+ Create policy" to start a new DLP policy +4. Choose a template (Financial data, Health data, Privacy, Custom, etc.) or create a custom policy +5. Define sensitive information types (SITs) to detect (credit card numbers, SSN, bank account numbers, etc.) +6. Configure rule conditions (locations, conditions for detection, scope) +7. Set enforcement actions (notify users, restrict access, block sharing, etc.) +8. Choose enforcement mode: + - Test mode (audit-only): Monitors but does not block activities + - Enforce mode: Blocks activities matching policy rules +9. Enable the policy and deploy to workloads (Exchange, SharePoint, OneDrive, Teams) +10. Monitor DLP alerts and adjust rules as needed + +Alternatively, create via PowerShell: +1. Connect to Exchange Online: `Connect-ExchangeOnline` +2. Create a policy: `New-DlpCompliancePolicy -Name "Sensitive Data Protection" -Mode "Enforce"` +3. Add rules to the policy: `New-DlpComplianceRule -Name "Block SSN" -Policy "Sensitive Data Protection"` +4. Enable and test: `Get-DlpCompliancePolicy | Select-Object -Property Name, Enabled` + +[Create and configure DLP policies](https://learn.microsoft.com/en-us/purview/dlp-create-deploy-policy) +[DLP policy templates](https://learn.microsoft.com/en-us/purview/dlp-policy-templates) +[DLP Compliance Rules](https://learn.microsoft.com/en-us/powershell/module/exchange/new-dlpcompliancerule) + +%TestResult% diff --git a/src/powershell/tests/Test-Assessment.35030.ps1 b/src/powershell/tests/Test-Assessment.35030.ps1 new file mode 100644 index 000000000..98ef354df --- /dev/null +++ b/src/powershell/tests/Test-Assessment.35030.ps1 @@ -0,0 +1,123 @@ +<# +.SYNOPSIS + Data Loss Prevention (DLP) Policies + +.DESCRIPTION + Data Loss Prevention (DLP) policies protect sensitive information by monitoring, detecting, and preventing the sharing of confidential data across Microsoft 365 workloads including Exchange Online, SharePoint Online, OneDrive, and Microsoft Teams. When DLP policies are not enabled or configured, organizations lack automated controls to prevent accidental or intentional disclosure of sensitive information such as credit card numbers, social security numbers, financial data, or proprietary information. Without active DLP policies, employees can freely share sensitive content through email, file uploads, or team communications without organizational oversight, increasing the risk of data breaches, regulatory violations (GDPR, HIPAA, PCI-DSS), and reputational damage. Enabling and configuring at least one DLP policy ensures organizations have automated detection and response capabilities for sensitive data, reducing the risk of unauthorized data exfiltration and demonstrating compliance readiness to regulators and auditors. + +.NOTES + Test ID: 35030 + Pillar: Data + Risk Level: High +#> + +function Test-Assessment-35030 { + [ZtTest( + Category = 'Data Loss Prevention (DLP)', + ImplementationCost = 'Medium', + MinimumLicense = ('Microsoft 365 E3'), + Pillar = 'Data', + RiskLevel = 'High', + SfiPillar = 'Protect tenants and production systems', + TenantType = ('Workforce'), + TestId = 35030, + Title = 'DLP Policies Cloud Workloads', + UserImpact = 'Medium' + )] + [CmdletBinding()] + param() + + #region Data Collection + Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose + + $activity = 'Checking Data Loss Prevention Policies' + Write-ZtProgress -Activity $activity -Status 'Querying DLP policies from compliance center' + + $dlpPolicies = $null + $dlpPoliciesDetailed = $null + $enabledPoliciesCount = 0 + $errorMsg = $null + + try { + # Q1: Get all DLP policies in the organization + $dlpPolicies = Get-DlpCompliancePolicy -ErrorAction Stop + + # Q2: Get details on DLP policy status and rule count + $dlpPoliciesDetailed = $dlpPolicies | Select-Object -Property Name, Enabled, WhenCreatedUTC, WhenChangedUTC + + # Q3: Count enabled vs disabled DLP policies + $enabledPoliciesCount = @($dlpPolicies | Where-Object { $_.Enabled -eq $true }).Count + } + catch { + $errorMsg = $_ + Write-PSFMessage "Error querying DLP policies: $_" -Level Error + } + #endregion Data Collection + + #region Assessment Logic + $investigateFlag = $false + $passed = $false + + if ($errorMsg) { + $investigateFlag = $true + } + else { + # If enabled policy count >= 1, the test passes + if ($enabledPoliciesCount -ge 1) { + $passed = $true + } + else { + # No policies exist or all policies are disabled + $passed = $false + } + } + #endregion Assessment Logic + + #region Report Generation + $testResultMarkdown = "" + + if ($investigateFlag) { + $testResultMarkdown = "⚠️ Unable to determine DLP policy status due to permissions issues or service connection failure.`n`n" + } + else { + if ($passed) { + $testResultMarkdown = "✅ One or more DLP policies are enabled and configured, providing automated protection against sensitive data disclosure.`n`n" + } + else { + $testResultMarkdown = "❌ No DLP policies are enabled or no DLP policies exist in the organization.`n`n" + } + + $testResultMarkdown += "## Data Loss Prevention Policy Summary`n`n" + $testResultMarkdown += "**Total DLP Policies:** $($dlpPolicies.Count)`n`n" + $testResultMarkdown += "**Enabled Policies:** $enabledPoliciesCount`n`n" + + if ($dlpPoliciesDetailed.Count -gt 0) { + $testResultMarkdown += "### DLP Policies Configuration`n`n" + $testResultMarkdown += "| Policy Name | Enabled Status | Created Date | Last Modified Date |`n" + $testResultMarkdown += "| :--- | :--- | :--- | :--- |`n" + + foreach ($policy in $dlpPoliciesDetailed) { + $enabledStatus = if ($policy.Enabled) { "✅ Yes" } else { "❌ No" } + $createdDate = if ($policy.WhenCreatedUTC) { $policy.WhenCreatedUTC.ToString('yyyy-MM-dd') } else { "N/A" } + $modifiedDate = if ($policy.WhenChangedUTC) { $policy.WhenChangedUTC.ToString('yyyy-MM-dd') } else { "N/A" } + $testResultMarkdown += "| $($policy.Name) | $enabledStatus | $createdDate | $modifiedDate |`n" + } + $testResultMarkdown += "`n" + } + + $testResultMarkdown += "[View DLP Policies in Microsoft Purview Portal](https://purview.microsoft.com/datalossprevention/policies)`n" + } + + $testResultMarkdown += "[View DLP Policies in Microsoft Purview Portal](https://purview.microsoft.com/datalossprevention/policies)`n" + #endregion Report Generation + + $params = @{ + TestId = '35030' + Status = $passed + Result = $testResultMarkdown + } + if ($investigateFlag -eq $true) { + $params.CustomStatus = 'Investigate' + } + Add-ZtTestResultDetail @params +} From 20d428a346fa3b367446e35c025249408c438c6d Mon Sep 17 00:00:00 2001 From: Afif Ahmed Patel Date: Thu, 8 Jan 2026 16:54:41 +0530 Subject: [PATCH 34/49] Refine segment scope column logic for assessment 25395 --- src/powershell/tests/Test-Assessment.25395.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25395.ps1 b/src/powershell/tests/Test-Assessment.25395.ps1 index 9753b8e2b..31ca66f74 100644 --- a/src/powershell/tests/Test-Assessment.25395.ps1 +++ b/src/powershell/tests/Test-Assessment.25395.ps1 @@ -207,7 +207,7 @@ function Test-Assessment-25395 { # Step 2: Evaluate segment destination granularity $issues = @() - $segmentSummary += "$($segment.destinationType):$($segment.destinationHost):$($segment.ports -join ',')" + $segmentSummary += "$($segment.destinationHost):$($segment.ports -join ',')" switch ($segment.destinationType) { 'dnsSuffix' { @@ -372,15 +372,15 @@ function Test-Assessment-25395 { $formatTemplate = @' ## Segment findings -| App name | Segment id | Issue | Destination | Ports | Recommendation | -|---|---|---|---|---|---| +| App name | Issue | Destination | Ports | Recommendation | +|---|---|---|---|---| {0} '@ foreach ($f in $segmentFindings) { $appLink = "https://entra.microsoft.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/overview/appId/$($f.AppId)" $linkedAppName = "[{0}]({1})" -f (Get-SafeMarkdown $f.AppName), $appLink - $tableRows += "| $linkedAppName | $($f.SegmentId) | $($f.Issue) | $($f.Destination) | $($f.Ports) | Narrow destination and ports |`n" + $tableRows += "| $linkedAppName | $($f.Issue) | $($f.Destination) | $($f.Ports) | Narrow destination and ports |`n" } $mdInfo += $formatTemplate -f $tableRows } From 5349e01ff6e69e07f608edc44f6406d1cd68cebe Mon Sep 17 00:00:00 2001 From: Komal Date: Thu, 8 Jan 2026 17:15:26 +0530 Subject: [PATCH 35/49] remove duplicate link --- src/powershell/tests/Test-Assessment.35030.ps1 | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.35030.ps1 b/src/powershell/tests/Test-Assessment.35030.ps1 index 98ef354df..92e227be7 100644 --- a/src/powershell/tests/Test-Assessment.35030.ps1 +++ b/src/powershell/tests/Test-Assessment.35030.ps1 @@ -3,7 +3,8 @@ Data Loss Prevention (DLP) Policies .DESCRIPTION - Data Loss Prevention (DLP) policies protect sensitive information by monitoring, detecting, and preventing the sharing of confidential data across Microsoft 365 workloads including Exchange Online, SharePoint Online, OneDrive, and Microsoft Teams. When DLP policies are not enabled or configured, organizations lack automated controls to prevent accidental or intentional disclosure of sensitive information such as credit card numbers, social security numbers, financial data, or proprietary information. Without active DLP policies, employees can freely share sensitive content through email, file uploads, or team communications without organizational oversight, increasing the risk of data breaches, regulatory violations (GDPR, HIPAA, PCI-DSS), and reputational damage. Enabling and configuring at least one DLP policy ensures organizations have automated detection and response capabilities for sensitive data, reducing the risk of unauthorized data exfiltration and demonstrating compliance readiness to regulators and auditors. + Data Loss Prevention (DLP) policies protect sensitive information by monitoring, detecting, and preventing the sharing of confidential data across Microsoft 365 workloads including Exchange Online, SharePoint Online, OneDrive, and Microsoft Teams. + When DLP policies are not enabled or configured, organizations lack automated controls to prevent accidental or intentional disclosure of sensitive information such as credit card numbers, social security numbers, financial data, or proprietary information. Without active DLP policies, employees can freely share sensitive content through email, file uploads, or team communications without organizational oversight, increasing the risk of data breaches, regulatory violations (GDPR, HIPAA, PCI-DSS), and reputational damage. Enabling and configuring at least one DLP policy ensures organizations have automated detection and response capabilities for sensitive data, reducing the risk of unauthorized data exfiltration and demonstrating compliance readiness to regulators and auditors. .NOTES Test ID: 35030 @@ -104,8 +105,6 @@ function Test-Assessment-35030 { } $testResultMarkdown += "`n" } - - $testResultMarkdown += "[View DLP Policies in Microsoft Purview Portal](https://purview.microsoft.com/datalossprevention/policies)`n" } $testResultMarkdown += "[View DLP Policies in Microsoft Purview Portal](https://purview.microsoft.com/datalossprevention/policies)`n" From 3164c9761b130cc28b66c9b879d48cbec8975e3f Mon Sep 17 00:00:00 2001 From: alexandair Date: Thu, 8 Jan 2026 12:45:21 +0000 Subject: [PATCH 36/49] Fix formatting of DLP policy links in Test-Assessment.35030.md --- src/powershell/tests/Test-Assessment.35030.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.35030.md b/src/powershell/tests/Test-Assessment.35030.md index 0cdb339d7..7ffc45f8e 100644 --- a/src/powershell/tests/Test-Assessment.35030.md +++ b/src/powershell/tests/Test-Assessment.35030.md @@ -27,8 +27,8 @@ Alternatively, create via PowerShell: 3. Add rules to the policy: `New-DlpComplianceRule -Name "Block SSN" -Policy "Sensitive Data Protection"` 4. Enable and test: `Get-DlpCompliancePolicy | Select-Object -Property Name, Enabled` -[Create and configure DLP policies](https://learn.microsoft.com/en-us/purview/dlp-create-deploy-policy) -[DLP policy templates](https://learn.microsoft.com/en-us/purview/dlp-policy-templates) -[DLP Compliance Rules](https://learn.microsoft.com/en-us/powershell/module/exchange/new-dlpcompliancerule) +- [Create and configure DLP policies](https://learn.microsoft.com/en-us/purview/dlp-create-deploy-policy) +- [DLP policy templates](https://learn.microsoft.com/en-us/purview/dlp-policy-templates) +- [DLP Compliance Rules](https://learn.microsoft.com/en-us/powershell/module/exchange/new-dlpcompliancerule) %TestResult% From e65ba67dd56b04ced2804ef48c52a2e5c5823674 Mon Sep 17 00:00:00 2001 From: alexandair Date: Thu, 8 Jan 2026 12:54:13 +0000 Subject: [PATCH 37/49] Update DLP policy title and optimize enabled policies count query --- src/powershell/tests/Test-Assessment.35030.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.35030.ps1 b/src/powershell/tests/Test-Assessment.35030.ps1 index 92e227be7..6c53d5467 100644 --- a/src/powershell/tests/Test-Assessment.35030.ps1 +++ b/src/powershell/tests/Test-Assessment.35030.ps1 @@ -22,7 +22,7 @@ function Test-Assessment-35030 { SfiPillar = 'Protect tenants and production systems', TenantType = ('Workforce'), TestId = 35030, - Title = 'DLP Policies Cloud Workloads', + Title = 'DLP Policies Enabled', UserImpact = 'Medium' )] [CmdletBinding()] @@ -47,7 +47,7 @@ function Test-Assessment-35030 { $dlpPoliciesDetailed = $dlpPolicies | Select-Object -Property Name, Enabled, WhenCreatedUTC, WhenChangedUTC # Q3: Count enabled vs disabled DLP policies - $enabledPoliciesCount = @($dlpPolicies | Where-Object { $_.Enabled -eq $true }).Count + $enabledPoliciesCount = @($dlpPolicies | Where-Object Enabled).Count } catch { $errorMsg = $_ From 8f8a6f6d0cc617fb4892037f76655920a5327c47 Mon Sep 17 00:00:00 2001 From: Ashwini Karke Date: Fri, 9 Jan 2026 09:04:04 +0530 Subject: [PATCH 38/49] added test --- .../tests/Test-Assessment.35025.ps1 | 45 ++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.35025.ps1 b/src/powershell/tests/Test-Assessment.35025.ps1 index 43db429fc..7aecf0c8e 100644 --- a/src/powershell/tests/Test-Assessment.35025.ps1 +++ b/src/powershell/tests/Test-Assessment.35025.ps1 @@ -87,14 +87,49 @@ function Test-Assessment-35025 { $testResultMarkdown = "❌ Internal RMS licensing is not enabled or licensing endpoints are not configured.`n`n" } - # Build detailed information table if we have data + # Build detailed information if we have data if ($irmConfig) { - $testResultMarkdown += "## [Internal RMS licensing configuration](https://purview.microsoft.com/settings/encryption)`n`n" + $testResultMarkdown += "## Internal RMS Licensing Status`n`n" $testResultMarkdown += "| Setting | Status |`n" $testResultMarkdown += "| :--- | :--- |`n" - $testResultMarkdown += "| Internal licensing enabled | $(if ($irmConfig.InternalLicensingEnabled -eq $true) { '✅ enabled' } elseif ($irmConfig.InternalLicensingEnabled -eq $false) { '❌ disabled' } else { '⚠️ unknown' }) |`n" - $testResultMarkdown += "| External licensing enabled | $(if ($irmConfig.ExternalLicensingEnabled -eq $true) { '✅ enabled' } elseif ($irmConfig.ExternalLicensingEnabled -eq $false) { '❌ disabled' } else { '⚠️ unknown' }) |`n" - $testResultMarkdown += "| Azure RMS licensing enabled | $(if ($irmConfig.AzureRMSLicensingEnabled -eq $true) { '✅ enabled' } elseif ($irmConfig.AzureRMSLicensingEnabled -eq $false) { '❌ disabled' } else { '⚠️ unknown' }) |`n" + + # Internal Licensing Enabled + $internalLicensing = if ($irmConfig.InternalLicensingEnabled -eq $true) { + '✅ True' + } elseif ($irmConfig.InternalLicensingEnabled -eq $false) { + '❌ False' + } else { + '⚠️ Unknown' + } + $testResultMarkdown += "| Internal licensing enabled | $internalLicensing |`n" + + # Intranet Distribution Point URL (ServiceLocation) + $serviceLocation = if ($irmConfig.ServiceLocation) { + Get-SafeMarkdown $irmConfig.ServiceLocation + } else { + 'Not configured' + } + $testResultMarkdown += "| Intranet distribution point URL | $serviceLocation |`n" + + # License Certification URL (LicensingLocation) + $licensingLocation = if ($irmConfig.LicensingLocation) { + ($irmConfig.LicensingLocation | ForEach-Object { Get-SafeMarkdown $_ }) -join ', ' + } else { + 'Not configured' + } + $testResultMarkdown += "| License certification URL | $licensingLocation |`n" + + # Internal Template Distribution (PublishingLocation or Azure-based) + $templateDistribution = if ($irmConfig.PublishingLocation) { + # Hybrid/on-premises: explicit publishing location configured + 'Configured' + } elseif ($irmConfig.InternalLicensingEnabled -eq $true -and $irmConfig.AzureRMSLicensingEnabled -eq $true) { + # Cloud-only: templates distributed via Azure RMS automatically + 'Configured' + } else { + 'Not Configured' + } + $testResultMarkdown += "| Internal template distribution | $templateDistribution |`n" } #endregion Report Generation From 16858490153254d9faf6cddd42167d146f78c57b Mon Sep 17 00:00:00 2001 From: Ashwini Karke Date: Fri, 9 Jan 2026 14:04:55 +0530 Subject: [PATCH 39/49] updated test --- .../tests/Test-Assessment.35025.ps1 | 110 ++++++++++-------- 1 file changed, 61 insertions(+), 49 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.35025.ps1 b/src/powershell/tests/Test-Assessment.35025.ps1 index 7aecf0c8e..cb3809218 100644 --- a/src/powershell/tests/Test-Assessment.35025.ps1 +++ b/src/powershell/tests/Test-Assessment.35025.ps1 @@ -32,7 +32,7 @@ function Test-Assessment-35025 { param() #region Data Collection - Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose + Write-PSFMessage 'Start Internal RMS Licensing evaluation' -Tag Test -Level VeryVerbose $activity = 'Checking Internal RMS Licensing Status' Write-ZtProgress -Activity $activity -Status 'Getting IRM configuration' @@ -75,61 +75,73 @@ function Test-Assessment-35025 { #endregion Assessment Logic #region Report Generation - $testResultMarkdown = '' - if ($customStatus -eq 'Investigate') { - $testResultMarkdown = "⚠️ Unable to determine internal RMS licensing status due to permissions issues or incomplete configuration data.`n`n" - } - elseif ($passed) { - $testResultMarkdown = "✅ Internal RMS licensing is enabled, allowing internal users to license and share protected content within the organization.`n`n" + $testResultMarkdown = "### Investigate`n`n" + $testResultMarkdown += "Unable to determine internal RMS licensing status due to permissions issues or incomplete configuration data." } else { - $testResultMarkdown = "❌ Internal RMS licensing is not enabled or licensing endpoints are not configured.`n`n" - } - - # Build detailed information if we have data - if ($irmConfig) { - $testResultMarkdown += "## Internal RMS Licensing Status`n`n" - $testResultMarkdown += "| Setting | Status |`n" - $testResultMarkdown += "| :--- | :--- |`n" - - # Internal Licensing Enabled - $internalLicensing = if ($irmConfig.InternalLicensingEnabled -eq $true) { - '✅ True' - } elseif ($irmConfig.InternalLicensingEnabled -eq $false) { - '❌ False' - } else { - '⚠️ Unknown' + if ($passed) { + $testResultMarkdown = "✅ Internal RMS licensing is enabled, allowing internal users to license and share protected content within the organization.`n`n" } - $testResultMarkdown += "| Internal licensing enabled | $internalLicensing |`n" - - # Intranet Distribution Point URL (ServiceLocation) - $serviceLocation = if ($irmConfig.ServiceLocation) { - Get-SafeMarkdown $irmConfig.ServiceLocation - } else { - 'Not configured' + else { + $testResultMarkdown = "❌ Internal RMS licensing is not enabled or licensing endpoints are not configured.`n`n" } - $testResultMarkdown += "| Intranet distribution point URL | $serviceLocation |`n" - # License Certification URL (LicensingLocation) - $licensingLocation = if ($irmConfig.LicensingLocation) { - ($irmConfig.LicensingLocation | ForEach-Object { Get-SafeMarkdown $_ }) -join ', ' - } else { - 'Not configured' - } - $testResultMarkdown += "| License certification URL | $licensingLocation |`n" - - # Internal Template Distribution (PublishingLocation or Azure-based) - $templateDistribution = if ($irmConfig.PublishingLocation) { - # Hybrid/on-premises: explicit publishing location configured - 'Configured' - } elseif ($irmConfig.InternalLicensingEnabled -eq $true -and $irmConfig.AzureRMSLicensingEnabled -eq $true) { - # Cloud-only: templates distributed via Azure RMS automatically - 'Configured' - } else { - 'Not Configured' + # Build detailed information if we have data + if ($irmConfig) { + # Prepare values first + $internalLicensingValue = if ($null -eq $irmConfig.InternalLicensingEnabled) { + 'Unknown' + } else { + $irmConfig.InternalLicensingEnabled + } + + $externalLicensingValue = if ($null -eq $irmConfig.ExternalLicensingEnabled) { + 'Unknown' + } else { + $irmConfig.ExternalLicensingEnabled + } + + $azureRMSLicensingValue = if ($null -eq $irmConfig.AzureRMSLicensingEnabled) { + 'Unknown' + } else { + $irmConfig.AzureRMSLicensingEnabled + } + + $licensingLocationValue = if ($irmConfig.LicensingLocation) { + ($irmConfig.LicensingLocation | ForEach-Object { Get-SafeMarkdown $_ }) -join ', ' + } else { + 'Not configured' + } + + $internalLicensingConfig = if ($irmConfig.InternalLicensingEnabled -eq $true) { + '✅ Enabled' + } elseif ($irmConfig.InternalLicensingEnabled -eq $false) { + '❌ Disabled' + } else { + '⚠️ Incomplete' + } + + $licensingEndpoints = if ($irmConfig.LicensingLocation) { + '✅ Configured' + } else { + '❌ Not Configured' + } + + # Build table + $testResultMarkdown += "**Internal RMS Licensing Status:**`n" + $testResultMarkdown += "| Setting | Status |`n" + $testResultMarkdown += "| :--- | :--- |`n" + $testResultMarkdown += "| InternalLicensingEnabled | $internalLicensingValue |`n" + $testResultMarkdown += "| ExternalLicensingEnabled | $externalLicensingValue |`n" + $testResultMarkdown += "| AzureRMSLicensingEnabled | $azureRMSLicensingValue |`n" + $testResultMarkdown += "| LicensingLocation | $licensingLocationValue |`n`n" + + # Summary section + $testResultMarkdown += "**Summary:**`n" + $testResultMarkdown += "* Internal Licensing Configuration: $internalLicensingConfig`n" + $testResultMarkdown += "* Licensing Endpoints: $licensingEndpoints`n" } - $testResultMarkdown += "| Internal template distribution | $templateDistribution |`n" } #endregion Report Generation From 92428bc0a888da4987e86fdf2802faf8855bf8d3 Mon Sep 17 00:00:00 2001 From: Ashwini Karke Date: Fri, 9 Jan 2026 17:04:24 +0530 Subject: [PATCH 40/49] added portal link --- src/powershell/tests/Test-Assessment.35025.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/powershell/tests/Test-Assessment.35025.ps1 b/src/powershell/tests/Test-Assessment.35025.ps1 index cb3809218..b027f13f0 100644 --- a/src/powershell/tests/Test-Assessment.35025.ps1 +++ b/src/powershell/tests/Test-Assessment.35025.ps1 @@ -129,7 +129,7 @@ function Test-Assessment-35025 { } # Build table - $testResultMarkdown += "**Internal RMS Licensing Status:**`n" + $testResultMarkdown += "**[Internal RMS Licensing Status](https://purview.microsoft.com/settings/encryption)**`n" $testResultMarkdown += "| Setting | Status |`n" $testResultMarkdown += "| :--- | :--- |`n" $testResultMarkdown += "| InternalLicensingEnabled | $internalLicensingValue |`n" From 961251fc8767627d11810df8bd5481a2db3d8e01 Mon Sep 17 00:00:00 2001 From: Ashwini Karke Date: Fri, 9 Jan 2026 22:04:19 +0530 Subject: [PATCH 41/49] fixed Copilot PR comments --- src/powershell/tests/Test-Assessment.35025.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.35025.ps1 b/src/powershell/tests/Test-Assessment.35025.ps1 index b027f13f0..984edf7d6 100644 --- a/src/powershell/tests/Test-Assessment.35025.ps1 +++ b/src/powershell/tests/Test-Assessment.35025.ps1 @@ -32,12 +32,12 @@ function Test-Assessment-35025 { param() #region Data Collection - Write-PSFMessage 'Start Internal RMS Licensing evaluation' -Tag Test -Level VeryVerbose + Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose $activity = 'Checking Internal RMS Licensing Status' Write-ZtProgress -Activity $activity -Status 'Getting IRM configuration' - # Q1: Get IRM licensing configuration + # Get IRM licensing configuration $irmConfig = $null $errorMsg = $null From 9a536c2bdadde60a99b50f1fedaac822035bf22c Mon Sep 17 00:00:00 2001 From: Komal Date: Mon, 12 Jan 2026 11:05:08 +0530 Subject: [PATCH 42/49] draft 35031 --- src/powershell/tests/Test-Assessment.35031.md | 34 +++++ .../tests/Test-Assessment.35031.ps1 | 133 ++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 src/powershell/tests/Test-Assessment.35031.md create mode 100644 src/powershell/tests/Test-Assessment.35031.ps1 diff --git a/src/powershell/tests/Test-Assessment.35031.md b/src/powershell/tests/Test-Assessment.35031.md new file mode 100644 index 000000000..0cdb339d7 --- /dev/null +++ b/src/powershell/tests/Test-Assessment.35031.md @@ -0,0 +1,34 @@ +Data Loss Prevention (DLP) policies protect sensitive information by monitoring, detecting, and preventing the sharing of confidential data across Microsoft 365 workloads including Exchange Online, SharePoint Online, OneDrive, and Microsoft Teams. + +When DLP policies are not enabled or configured, organizations lack automated controls to prevent accidental or intentional disclosure of sensitive information such as credit card numbers, social security numbers, financial data, or proprietary information. Without active DLP policies, employees can freely share sensitive content through email, file uploads, or team communications without organizational oversight, increasing the risk of data breaches, regulatory violations (GDPR, HIPAA, PCI-DSS), and reputational damage. + +Enabling and configuring at least one DLP policy ensures organizations have automated detection and response capabilities for sensitive data, reducing the risk of unauthorized data exfiltration and demonstrating compliance readiness to regulators and auditors. + +**Remediation action** + +To create and enable DLP policies: + +1. Sign in as a Global Administrator or Compliance Administrator to the [Microsoft Purview portal](https://purview.microsoft.com) +2. Navigate to Data Loss Prevention > Policies +3. Select "+ Create policy" to start a new DLP policy +4. Choose a template (Financial data, Health data, Privacy, Custom, etc.) or create a custom policy +5. Define sensitive information types (SITs) to detect (credit card numbers, SSN, bank account numbers, etc.) +6. Configure rule conditions (locations, conditions for detection, scope) +7. Set enforcement actions (notify users, restrict access, block sharing, etc.) +8. Choose enforcement mode: + - Test mode (audit-only): Monitors but does not block activities + - Enforce mode: Blocks activities matching policy rules +9. Enable the policy and deploy to workloads (Exchange, SharePoint, OneDrive, Teams) +10. Monitor DLP alerts and adjust rules as needed + +Alternatively, create via PowerShell: +1. Connect to Exchange Online: `Connect-ExchangeOnline` +2. Create a policy: `New-DlpCompliancePolicy -Name "Sensitive Data Protection" -Mode "Enforce"` +3. Add rules to the policy: `New-DlpComplianceRule -Name "Block SSN" -Policy "Sensitive Data Protection"` +4. Enable and test: `Get-DlpCompliancePolicy | Select-Object -Property Name, Enabled` + +[Create and configure DLP policies](https://learn.microsoft.com/en-us/purview/dlp-create-deploy-policy) +[DLP policy templates](https://learn.microsoft.com/en-us/purview/dlp-policy-templates) +[DLP Compliance Rules](https://learn.microsoft.com/en-us/powershell/module/exchange/new-dlpcompliancerule) + +%TestResult% diff --git a/src/powershell/tests/Test-Assessment.35031.ps1 b/src/powershell/tests/Test-Assessment.35031.ps1 new file mode 100644 index 000000000..1d16196b1 --- /dev/null +++ b/src/powershell/tests/Test-Assessment.35031.ps1 @@ -0,0 +1,133 @@ +<# +.SYNOPSIS + Endpoint Data Loss Prevention (DLP) Policies + +.DESCRIPTION + Endpoint Data Loss Prevention (Endpoint DLP) extends data protection from cloud workloads (Exchange, SharePoint, OneDrive, Teams) to user devices (Windows, macOS, Linux), creating comprehensive coverage for sensitive data across the entire organization. Endpoint DLP monitors and controls sensitive data activities on employee devices, preventing unauthorized data transfer to cloud storage, USB drives, printers, external applications, and removable media. Without Endpoint DLP policies configured, organizations have visibility and control only over cloud-based activities, leaving sensitive data vulnerable when accessed from user devices or transferred to unmanaged applications. Endpoint DLP policies integrate with Defender for Endpoint to provide rich telemetry about data handling activities, enabling organizations to detect and respond to data exfiltration attempts in real time. For organizations handling highly sensitive data (trade secrets, financial records, PII, healthcare information), Endpoint DLP is critical to preventing data loss through user devices and uncontrolled applications. Configuring at least one Endpoint DLP policy targeting critical sensitive information types (financial data, trade secrets, customer PII) ensures defense-in-depth protection that extends beyond cloud workloads. + +.NOTES + Test ID: 35031 + Pillar: Data + Risk Level: High +#> + +function Test-Assessment-35031 { + [ZtTest( + Category = 'Data Loss Prevention (DLP)', + ImplementationCost = 'Medium', + MinimumLicense = ('Microsoft 365 E3'), + Pillar = 'Data', + RiskLevel = 'High', + SfiPillar = 'Protect tenants and production systems', + TenantType = ('Workforce'), + TestId = 35031, + Title = 'DLP Policies Endpoint Workloads', + UserImpact = 'Medium' + )] + [CmdletBinding()] + param() + + #region Data Collection + Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose + + $activity = 'Checking Endpoint Data Loss Prevention Policies' + Write-ZtProgress -Activity $activity -Status 'Querying endpoint DLP policies from compliance center' + + $dlpPolicies = $null + $dlpPoliciesDetailed = $null + $endpointDlpPolicies = $null + $enabledEndpointPoliciesCount = 0 + $errorMsg = $null + + try { + # Q1: Get all DLP compliance policies + $dlpPolicies = Get-DlpCompliancePolicy -ErrorAction Stop + + # Q2: Filter for endpoint DLP policies + $endpointDlpPolicies = $dlpPolicies | Where-Object { $_.EndpointDlpLocation -ne $null } + + # Q3: Count enabled endpoint DLP policies + $enabledEndpointPoliciesCount = @($endpointDlpPolicies | Where-Object { $_.Enabled -eq $true }).Count + + # Get details on endpoint DLP policy status and rules + $dlpPoliciesDetailed = $endpointDlpPolicies | Select-Object -Property Name, Enabled, WhenCreatedUTC, WhenChangedUTC, EndpointDlpLocation, EndpointDlpRules + } + catch { + $errorMsg = $_ + Write-PSFMessage "Error querying DLP policies: $_" -Level Error + } + #endregion Data Collection + + #region Assessment Logic + $investigateFlag = $false + $passed = $false + + if ($errorMsg) { + $investigateFlag = $true + } + else { + # If enabled endpoint DLP policy count >= 1, the test passes + if ($enabledEndpointPoliciesCount -ge 1) { + $passed = $true + } + else { + # No endpoint policies exist or all endpoint policies are disabled + $passed = $false + } + } + #endregion Assessment Logic + + #region Report Generation + $testResultMarkdown = "" + + if ($investigateFlag) { + $testResultMarkdown = "⚠️ Unable to determine Endpoint DLP policy configuration due to permissions issues or service connection failure.`n`n" + } + else { + if ($passed) { + $testResultMarkdown = "✅ Endpoint DLP policies are configured and enabled, protecting sensitive data on user devices.`n`n" + } + else { + $testResultMarkdown = "❌ No Endpoint DLP policies are configured, or all endpoint policies are disabled.`n`n" + } + + $testResultMarkdown += "## Endpoint DLP Policy Summary`n`n" + $testResultMarkdown += "**Total DLP Policies:** $($dlpPolicies.Count)`n`n" + $testResultMarkdown += "**Endpoint DLP Policies:** $($endpointDlpPolicies.Count)`n`n" + $testResultMarkdown += "**Enabled Endpoint Policies:** $enabledEndpointPoliciesCount`n`n" + + if ($endpointDlpPolicies.Count -gt 0) { + $testResultMarkdown += "### Endpoint DLP Policies Configuration`n`n" + + foreach ($policy in $dlpPoliciesDetailed) { + $enabledStatus = if ($policy.Enabled) { "✅ Enabled" } else { "❌ Disabled" } + $endpointLocations = if ($policy.EndpointDlpLocation) { ($policy.EndpointDlpLocation -join ', ') } else { "None" } + $rulesCount = if ($policy.EndpointDlpRules) { @($policy.EndpointDlpRules).Count } else { 0 } + $createdDate = if ($policy.WhenCreatedUTC) { $policy.WhenCreatedUTC.ToString('yyyy-MM-dd') } else { "N/A" } + + $testResultMarkdown += "#### $($policy.Name)`n`n" + $testResultMarkdown += "* **Enabled Status:** $enabledStatus`n" + $testResultMarkdown += "* **Endpoint Locations:** $endpointLocations`n" + $testResultMarkdown += "* **Associated Rules Count:** $rulesCount`n" + $testResultMarkdown += "* **Created Date:** $createdDate`n`n" + } + } + else { + $testResultMarkdown += "### Endpoint DLP Policies Configuration`n`n" + $testResultMarkdown += "No endpoint DLP policies are currently configured in the organization. Organizations should configure at least one endpoint DLP policy protecting critical sensitive data types (financial records, trade secrets, PII).`n`n" + } + } + + $testResultMarkdown += "[View DLP Policies in Microsoft Purview Portal](https://purview.microsoft.com/datalossprevention/policies)`n" + #endregion Report Generation + + $params = @{ + TestId = '35031' + Status = $passed + Result = $testResultMarkdown + } + if ($investigateFlag -eq $true) { + $params.CustomStatus = 'Investigate' + } + Add-ZtTestResultDetail @params +} From 4fcf99f3c5335dc372567b500f885ea21e5a9f21 Mon Sep 17 00:00:00 2001 From: Komal Date: Mon, 12 Jan 2026 11:29:39 +0530 Subject: [PATCH 43/49] integrate Find-ZtProfileLinkedToPolicy --- .../tests/Test-Assessment.25411.ps1 | 138 +++++------------- 1 file changed, 39 insertions(+), 99 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25411.ps1 b/src/powershell/tests/Test-Assessment.25411.ps1 index 3250d4323..caa35b3f6 100644 --- a/src/powershell/tests/Test-Assessment.25411.ps1 +++ b/src/powershell/tests/Test-Assessment.25411.ps1 @@ -44,28 +44,6 @@ function Test-Assessment-25411 { 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 @@ -73,93 +51,55 @@ function Test-Assessment-25411 { $enabledSecurityProfiles = @() $enabledBaseLineProfiles = @() - # Iterate each TLS inspection policy and find linked profiles + # Iterate each TLS inspection policy and find linked profiles using the helper function foreach ($tlsPolicy in $tlsInspectionPolicies) { - $tlsId = $tlsPolicy.id - $baseLineProfileFound = $false - foreach ($profileItem in $filteringProfiles) { - $profilePolicies = @() - if ($null -ne $profileItem.policies) { - $profilePolicies = $profileItem.policies - } + $findParams = @{ + PolicyId = $tlsPolicy.id + FilteringProfiles = $filteringProfiles + CAPolicies = $allCAPolicies + BaselinePriority = $BASELINE_PROFILE_PRIORITY + PolicyLinkType = 'tlsInspectionPolicyLink' + PolicyRules = $tlsPolicy + } - 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 + $linkedProfiles = Find-ZtProfilesLinkedToPolicy @findParams + + foreach ($policyProfile in $linkedProfiles) { + if ($policyProfile.ProfileType -eq 'Baseline Profile' -and $policyProfile.PassesCriteria -and $policyProfile.ProfileState -eq 'enabled') { + $enabledBaseLineProfiles += [PSCustomObject]@{ + ProfileId = $policyProfile.ProfileId + ProfileName = $policyProfile.ProfileName + ProfileState = $policyProfile.ProfileState + ProfilePriority = $policyProfile.ProfilePriority + TLSPolicyId = $tlsPolicy.id + TLSPolicyName = $tlsPolicy.name + TLSPolicyLinkState = $policyProfile.PolicyLinkState + } + } + elseif ($policyProfile.ProfileType -eq 'Security Profile' -and $policyProfile.PassesCriteria -and $policyProfile.ProfileState -eq 'enabled') { + $matchedCAPolicies = @() + if ($null -ne $policyProfile.CAPolicy) { + $matchedCAPolicies = @($policyProfile.CAPolicy) } - if ($null -ne $linkedPolicyId -and $linkedPolicyId -eq $tlsId) { - $linkState = if ($null -ne $plink.state) { - $plink.state + $enabledSecurityProfiles += [PSCustomObject]@{ + ProfileId = $policyProfile.ProfileId + ProfileName = $policyProfile.ProfileName + ProfileState = $policyProfile.ProfileState + ProfilePriority = $policyProfile.ProfilePriority + TLSPolicyId = $tlsPolicy.id + TLSPolicyName = $tlsPolicy.name + TLSPolicyLinkState = $policyProfile.PolicyLinkState + MatchedCAPolicies = $matchedCAPolicies + CAPolicyCount = $matchedCAPolicies.Count + DefaultAction = if ($null -ne $tlsPolicy.settings) { + $tlsPolicy.settings.defaultAction } 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 - } } } From b841097eaa46f21442aee7b2d95215513c0cf68d Mon Sep 17 00:00:00 2001 From: Komal Date: Mon, 12 Jan 2026 11:32:38 +0530 Subject: [PATCH 44/49] Revert draft 35031 --- src/powershell/tests/Test-Assessment.35031.md | 34 ----- .../tests/Test-Assessment.35031.ps1 | 133 ------------------ 2 files changed, 167 deletions(-) delete mode 100644 src/powershell/tests/Test-Assessment.35031.md delete mode 100644 src/powershell/tests/Test-Assessment.35031.ps1 diff --git a/src/powershell/tests/Test-Assessment.35031.md b/src/powershell/tests/Test-Assessment.35031.md deleted file mode 100644 index 0cdb339d7..000000000 --- a/src/powershell/tests/Test-Assessment.35031.md +++ /dev/null @@ -1,34 +0,0 @@ -Data Loss Prevention (DLP) policies protect sensitive information by monitoring, detecting, and preventing the sharing of confidential data across Microsoft 365 workloads including Exchange Online, SharePoint Online, OneDrive, and Microsoft Teams. - -When DLP policies are not enabled or configured, organizations lack automated controls to prevent accidental or intentional disclosure of sensitive information such as credit card numbers, social security numbers, financial data, or proprietary information. Without active DLP policies, employees can freely share sensitive content through email, file uploads, or team communications without organizational oversight, increasing the risk of data breaches, regulatory violations (GDPR, HIPAA, PCI-DSS), and reputational damage. - -Enabling and configuring at least one DLP policy ensures organizations have automated detection and response capabilities for sensitive data, reducing the risk of unauthorized data exfiltration and demonstrating compliance readiness to regulators and auditors. - -**Remediation action** - -To create and enable DLP policies: - -1. Sign in as a Global Administrator or Compliance Administrator to the [Microsoft Purview portal](https://purview.microsoft.com) -2. Navigate to Data Loss Prevention > Policies -3. Select "+ Create policy" to start a new DLP policy -4. Choose a template (Financial data, Health data, Privacy, Custom, etc.) or create a custom policy -5. Define sensitive information types (SITs) to detect (credit card numbers, SSN, bank account numbers, etc.) -6. Configure rule conditions (locations, conditions for detection, scope) -7. Set enforcement actions (notify users, restrict access, block sharing, etc.) -8. Choose enforcement mode: - - Test mode (audit-only): Monitors but does not block activities - - Enforce mode: Blocks activities matching policy rules -9. Enable the policy and deploy to workloads (Exchange, SharePoint, OneDrive, Teams) -10. Monitor DLP alerts and adjust rules as needed - -Alternatively, create via PowerShell: -1. Connect to Exchange Online: `Connect-ExchangeOnline` -2. Create a policy: `New-DlpCompliancePolicy -Name "Sensitive Data Protection" -Mode "Enforce"` -3. Add rules to the policy: `New-DlpComplianceRule -Name "Block SSN" -Policy "Sensitive Data Protection"` -4. Enable and test: `Get-DlpCompliancePolicy | Select-Object -Property Name, Enabled` - -[Create and configure DLP policies](https://learn.microsoft.com/en-us/purview/dlp-create-deploy-policy) -[DLP policy templates](https://learn.microsoft.com/en-us/purview/dlp-policy-templates) -[DLP Compliance Rules](https://learn.microsoft.com/en-us/powershell/module/exchange/new-dlpcompliancerule) - -%TestResult% diff --git a/src/powershell/tests/Test-Assessment.35031.ps1 b/src/powershell/tests/Test-Assessment.35031.ps1 deleted file mode 100644 index 1d16196b1..000000000 --- a/src/powershell/tests/Test-Assessment.35031.ps1 +++ /dev/null @@ -1,133 +0,0 @@ -<# -.SYNOPSIS - Endpoint Data Loss Prevention (DLP) Policies - -.DESCRIPTION - Endpoint Data Loss Prevention (Endpoint DLP) extends data protection from cloud workloads (Exchange, SharePoint, OneDrive, Teams) to user devices (Windows, macOS, Linux), creating comprehensive coverage for sensitive data across the entire organization. Endpoint DLP monitors and controls sensitive data activities on employee devices, preventing unauthorized data transfer to cloud storage, USB drives, printers, external applications, and removable media. Without Endpoint DLP policies configured, organizations have visibility and control only over cloud-based activities, leaving sensitive data vulnerable when accessed from user devices or transferred to unmanaged applications. Endpoint DLP policies integrate with Defender for Endpoint to provide rich telemetry about data handling activities, enabling organizations to detect and respond to data exfiltration attempts in real time. For organizations handling highly sensitive data (trade secrets, financial records, PII, healthcare information), Endpoint DLP is critical to preventing data loss through user devices and uncontrolled applications. Configuring at least one Endpoint DLP policy targeting critical sensitive information types (financial data, trade secrets, customer PII) ensures defense-in-depth protection that extends beyond cloud workloads. - -.NOTES - Test ID: 35031 - Pillar: Data - Risk Level: High -#> - -function Test-Assessment-35031 { - [ZtTest( - Category = 'Data Loss Prevention (DLP)', - ImplementationCost = 'Medium', - MinimumLicense = ('Microsoft 365 E3'), - Pillar = 'Data', - RiskLevel = 'High', - SfiPillar = 'Protect tenants and production systems', - TenantType = ('Workforce'), - TestId = 35031, - Title = 'DLP Policies Endpoint Workloads', - UserImpact = 'Medium' - )] - [CmdletBinding()] - param() - - #region Data Collection - Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose - - $activity = 'Checking Endpoint Data Loss Prevention Policies' - Write-ZtProgress -Activity $activity -Status 'Querying endpoint DLP policies from compliance center' - - $dlpPolicies = $null - $dlpPoliciesDetailed = $null - $endpointDlpPolicies = $null - $enabledEndpointPoliciesCount = 0 - $errorMsg = $null - - try { - # Q1: Get all DLP compliance policies - $dlpPolicies = Get-DlpCompliancePolicy -ErrorAction Stop - - # Q2: Filter for endpoint DLP policies - $endpointDlpPolicies = $dlpPolicies | Where-Object { $_.EndpointDlpLocation -ne $null } - - # Q3: Count enabled endpoint DLP policies - $enabledEndpointPoliciesCount = @($endpointDlpPolicies | Where-Object { $_.Enabled -eq $true }).Count - - # Get details on endpoint DLP policy status and rules - $dlpPoliciesDetailed = $endpointDlpPolicies | Select-Object -Property Name, Enabled, WhenCreatedUTC, WhenChangedUTC, EndpointDlpLocation, EndpointDlpRules - } - catch { - $errorMsg = $_ - Write-PSFMessage "Error querying DLP policies: $_" -Level Error - } - #endregion Data Collection - - #region Assessment Logic - $investigateFlag = $false - $passed = $false - - if ($errorMsg) { - $investigateFlag = $true - } - else { - # If enabled endpoint DLP policy count >= 1, the test passes - if ($enabledEndpointPoliciesCount -ge 1) { - $passed = $true - } - else { - # No endpoint policies exist or all endpoint policies are disabled - $passed = $false - } - } - #endregion Assessment Logic - - #region Report Generation - $testResultMarkdown = "" - - if ($investigateFlag) { - $testResultMarkdown = "⚠️ Unable to determine Endpoint DLP policy configuration due to permissions issues or service connection failure.`n`n" - } - else { - if ($passed) { - $testResultMarkdown = "✅ Endpoint DLP policies are configured and enabled, protecting sensitive data on user devices.`n`n" - } - else { - $testResultMarkdown = "❌ No Endpoint DLP policies are configured, or all endpoint policies are disabled.`n`n" - } - - $testResultMarkdown += "## Endpoint DLP Policy Summary`n`n" - $testResultMarkdown += "**Total DLP Policies:** $($dlpPolicies.Count)`n`n" - $testResultMarkdown += "**Endpoint DLP Policies:** $($endpointDlpPolicies.Count)`n`n" - $testResultMarkdown += "**Enabled Endpoint Policies:** $enabledEndpointPoliciesCount`n`n" - - if ($endpointDlpPolicies.Count -gt 0) { - $testResultMarkdown += "### Endpoint DLP Policies Configuration`n`n" - - foreach ($policy in $dlpPoliciesDetailed) { - $enabledStatus = if ($policy.Enabled) { "✅ Enabled" } else { "❌ Disabled" } - $endpointLocations = if ($policy.EndpointDlpLocation) { ($policy.EndpointDlpLocation -join ', ') } else { "None" } - $rulesCount = if ($policy.EndpointDlpRules) { @($policy.EndpointDlpRules).Count } else { 0 } - $createdDate = if ($policy.WhenCreatedUTC) { $policy.WhenCreatedUTC.ToString('yyyy-MM-dd') } else { "N/A" } - - $testResultMarkdown += "#### $($policy.Name)`n`n" - $testResultMarkdown += "* **Enabled Status:** $enabledStatus`n" - $testResultMarkdown += "* **Endpoint Locations:** $endpointLocations`n" - $testResultMarkdown += "* **Associated Rules Count:** $rulesCount`n" - $testResultMarkdown += "* **Created Date:** $createdDate`n`n" - } - } - else { - $testResultMarkdown += "### Endpoint DLP Policies Configuration`n`n" - $testResultMarkdown += "No endpoint DLP policies are currently configured in the organization. Organizations should configure at least one endpoint DLP policy protecting critical sensitive data types (financial records, trade secrets, PII).`n`n" - } - } - - $testResultMarkdown += "[View DLP Policies in Microsoft Purview Portal](https://purview.microsoft.com/datalossprevention/policies)`n" - #endregion Report Generation - - $params = @{ - TestId = '35031' - Status = $passed - Result = $testResultMarkdown - } - if ($investigateFlag -eq $true) { - $params.CustomStatus = 'Investigate' - } - Add-ZtTestResultDetail @params -} From 577566910402719eef32f2ef2b2df00d57c11006 Mon Sep 17 00:00:00 2001 From: Ashwini Karke Date: Mon, 12 Jan 2026 12:51:04 +0530 Subject: [PATCH 45/49] updated portal link --- src/powershell/tests/Test-Assessment.35025.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.35025.md b/src/powershell/tests/Test-Assessment.35025.md index f8ba02368..8ae5cc3e5 100644 --- a/src/powershell/tests/Test-Assessment.35025.md +++ b/src/powershell/tests/Test-Assessment.35025.md @@ -16,8 +16,8 @@ For organizations using Exchange Online, ensure mail flow policies and RMS featu 2. Verify internal licensing is enabled: `Set-IRMConfiguration -InternalLicensingEnabled $true` 3. Verify the setting: `Get-IRMConfiguration | Select-Object -Property InternalLicensingEnabled, ExternalLicensingEnabled` -- [Configure Azure Rights Management licensing](https://learn.microsoft.com/en-us/purview/encryption-configure-aip) -- [Rights Management in Exchange Online](https://learn.microsoft.com/en-us/purview/encryption-exchange-owa) +- [Configure Azure Rights Management licensing](https://learn.microsoft.com/en-us/purview/set-up-new-message-encryption-capabilities) +- [Rights Management in Exchange Online](https://learn.microsoft.com/en-us/purview/information-rights-management-in-exchange-online) %TestResult% From 6fdd45926893895103d7179fecb264786cf4e17c Mon Sep 17 00:00:00 2001 From: alexandair Date: Mon, 12 Jan 2026 11:07:07 +0000 Subject: [PATCH 46/49] Add Find-ZtProfilesLinkedToPolicy function to evaluate linked filtering profiles --- .../graph/Find-ZtProfilesLinkedToPolicy.ps1 | 195 ++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 src/powershell/private/graph/Find-ZtProfilesLinkedToPolicy.ps1 diff --git a/src/powershell/private/graph/Find-ZtProfilesLinkedToPolicy.ps1 b/src/powershell/private/graph/Find-ZtProfilesLinkedToPolicy.ps1 new file mode 100644 index 000000000..bf9d995ed --- /dev/null +++ b/src/powershell/private/graph/Find-ZtProfilesLinkedToPolicy.ps1 @@ -0,0 +1,195 @@ +function Find-ZtProfilesLinkedToPolicy { + <# + .SYNOPSIS + Finds filtering profiles that are linked to a specific policy and evaluates if they meet pass criteria. + + .DESCRIPTION + This function searches through Global Secure Access filtering profiles to find those linked to a specific policy. + It evaluates whether each linked profile meets the pass criteria based on profile type: + - Baseline Profile (priority = 65000): Passes automatically regardless of link state + - Security Profile (priority < 65000): Passes only if linked to an enabled Conditional Access policy + + .PARAMETER PolicyId + The ID of the filtering policy to search for. + + .PARAMETER FilteringProfiles + Collection of all filtering profiles to search through. + + .PARAMETER CAPolicies + Collection of Conditional Access policies for Security Profile validation. + + .PARAMETER BaselinePriority + The priority value that identifies the Baseline Profile (typically 65000). + + .PARAMETER PolicyLinkType + The type of policy link to search for. Valid values: + - filteringPolicyLink (Web Content Filtering) + - tlsInspectionPolicyLink (TLS Inspection) + - filePolicyLink (File Policy) + - promptPolicyLink (Prompt Policy) + + .PARAMETER PolicyRules + Collection of policy rules associated with the policy (e.g., webCategory rules, TLS inspection rules). + + .EXAMPLE + $findParams = @{ + PolicyId = $policyId + FilteringProfiles = $filteringProfiles + CAPolicies = $caPolicies + BaselinePriority = 65000 + PolicyLinkType = 'filteringPolicyLink' + PolicyRules = $webCategoryRules + } + $linkedProfiles = Find-ZtProfilesLinkedToPolicy @findParams + + .OUTPUTS + Array of PSCustomObject with the following properties: + - ProfileId: The profile ID + - ProfileName: The profile name + - ProfileType: 'Baseline Profile' or 'Security Profile' + - ProfileState: The profile state + - ProfilePriority: The profile priority value + - PolicyLinkState: The state of the policy link (enabled/disabled/unknown) + - PassesCriteria: Boolean indicating if the profile meets pass criteria + - CAPolicy: Linked Conditional Access policies (for Security Profiles only) + - PolicyRules: The policy rules passed in + + .NOTES + This function is used by Global Secure Access assessment tests to evaluate policy enforcement. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$PolicyId, + + [Parameter(Mandatory)] + [AllowEmptyCollection()] + [object[]]$FilteringProfiles, + + [Parameter(Mandatory)] + [AllowEmptyCollection()] + [object[]]$CAPolicies, + + [Parameter(Mandatory)] + [int]$BaselinePriority, + + [Parameter(Mandatory)] + [ValidateSet('filteringPolicyLink', 'tlsInspectionPolicyLink', 'filePolicyLink', 'promptPolicyLink')] + [string]$PolicyLinkType, + + [Parameter(Mandatory)] + [AllowEmptyCollection()] + [object[]]$PolicyRules + ) + + # OData type lookup for type safety + $odataTypeMap = @{ + 'filteringPolicyLink' = '#microsoft.graph.networkaccess.filteringPolicyLink' + 'tlsInspectionPolicyLink' = '#microsoft.graph.networkaccess.tlsInspectionPolicyLink' + 'filePolicyLink' = '#microsoft.graph.networkaccess.filePolicyLink' + 'promptPolicyLink' = '#microsoft.graph.networkaccess.promptPolicyLink' + } + + $odataType = $odataTypeMap[$PolicyLinkType] + if (-not $odataType) { + Write-PSFMessage "Unknown PolicyLinkType: $PolicyLinkType" -Tag Test -Level Warning + return @() + } + + $linkedProfiles = [System.Collections.Generic.List[PSCustomObject]]::new() + + foreach ($filteringProfile in $FilteringProfiles) { + # Get profile policies safely + $profilePolicies = @() + if ($null -ne $filteringProfile.policies) { + # Force array to handle both scalar and array returns from Graph API + $profilePolicies = @($filteringProfile.policies) + } + + foreach ($policyLink in $profilePolicies) { + $plinkType = $policyLink.'@odata.type' + $linkedPolicyId = $null + + # Only process the specified policy link type + if ($plinkType -eq $odataType -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) { + Write-PSFMessage "Skipping profile '$($filteringProfile.name)' (ID: $($filteringProfile.id)) - missing priority property" -Tag Test -Level Debug + continue + } + + $linkState = if ($null -ne $policyLink.state) { + $policyLink.state + } + else { + 'unknown' + } + + if ($priority -eq $BaselinePriority) { + # Baseline Profile: passes regardless of enabled state per spec + $profileInfo = [PSCustomObject]@{ + ProfileId = $filteringProfile.id + ProfileName = $filteringProfile.name + ProfileType = 'Baseline Profile' + ProfileState = $filteringProfile.state + ProfilePriority = $priority + PolicyLinkState = $linkState + PassesCriteria = $true + CAPolicy = $null + PolicyRules = $PolicyRules + } + $linkedProfiles.Add($profileInfo) | Out-Null + } + elseif ($priority -lt $BaselinePriority) { + # Security Profile: check if linked to enabled CA policy + $linkedCAPolicies = $CAPolicies | Where-Object { + # Use null-conditional operator for safe navigation + $_.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 + PolicyRules = $PolicyRules + } + + if ($linkedCAPolicies) { + # Check if at least one CA policy is enabled + $enabledCAPolicies = $linkedCAPolicies | Where-Object { $_.state -eq 'enabled' } + if ($enabledCAPolicies) { + $profileInfo.PassesCriteria = $true + } + $profileInfo.CAPolicy = $linkedCAPolicies + } + + $linkedProfiles.Add($profileInfo) | Out-Null + } + else { + # Priority > BaselinePriority + Write-PSFMessage "Skipping profile '$($filteringProfile.name)' (ID: $($filteringProfile.id)) - unexpected priority value: $priority (expected <= $BaselinePriority)" -Tag Test -Level Debug + } + } + } + } + + return $linkedProfiles +} From 6f0b0f87b985425080d81b69b85040c949e1b51f Mon Sep 17 00:00:00 2001 From: Komal Date: Wed, 7 Jan 2026 12:53:05 +0530 Subject: [PATCH 47/49] add support for AIPService --- .../public/Connect-ZtAssessment.ps1 | 44 +++++- .../tests/Test-Assessment.35011.ps1 | 144 ++++++++++++++++++ 2 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 src/powershell/tests/Test-Assessment.35011.ps1 diff --git a/src/powershell/public/Connect-ZtAssessment.ps1 b/src/powershell/public/Connect-ZtAssessment.ps1 index 9d66eeb90..3b23b6caa 100644 --- a/src/powershell/public/Connect-ZtAssessment.ps1 +++ b/src/powershell/public/Connect-ZtAssessment.ps1 @@ -83,7 +83,7 @@ $SkipAzureConnection, # The services to connect to such as Azure and ExchangeOnline. Default is Graph. - [ValidateSet('All', 'Azure', 'ExchangeOnline', 'Graph', 'SecurityCompliance', 'SharePointOnline')] + [ValidateSet('All', 'Azure', 'AipService', 'ExchangeOnline', 'Graph', 'SecurityCompliance', 'SharePointOnline')] [string[]]$Service = 'Graph', # The Exchange environment to connect to. Default is O365Default. Supported values include O365China, O365Default, O365GermanyCloud, O365USGovDoD, O365USGovGCCHigh. @@ -117,7 +117,7 @@ } - $OrderedImport = Get-ModuleImportOrder -Name @('Az.Accounts', 'ExchangeOnlineManagement', 'Microsoft.Graph.Authentication', 'Microsoft.Online.SharePoint.PowerShell') + $OrderedImport = Get-ModuleImportOrder -Name @('Az.Accounts', 'ExchangeOnlineManagement', 'Microsoft.Graph.Authentication', 'Microsoft.Online.SharePoint.PowerShell', 'AipService') Write-Verbose "Import Order: $($OrderedImport.Name -join ', ')" @@ -348,6 +348,32 @@ } } } + + 'AipService' { + if ($Service -contains 'AipService' -or $Service -contains 'All') { + try { + # Import module with compatibility if needed + if ($PSVersionTable.PSEdition -ne 'Desktop') { + # Assume module is installed in Windows PowerShell as per instructions + Import-Module AipService -UseWindowsPowerShell -WarningAction SilentlyContinue -ErrorAction Stop -Global + } + else { + Import-Module AipService -ErrorAction Stop -Global + } + } + catch { + # Provide clearer guidance when import fails, especially under PowerShell Core + if ($PSVersionTable.PSEdition -ne 'Desktop') { + $message = "Failed to import AipService module. When running in PowerShell Core, 'AipService' must be installed in Windows PowerShell 5.1 (Desktop) for -UseWindowsPowerShell to work. Underlying error: $_" + } + else { + $message = "Failed to import AipService module: $_" + } + Write-Host "`n$message" -ForegroundColor Red + Write-PSFMessage $message -Level Error + } + } + } } if ($Service -contains 'SharePointOnline' -or $Service -contains 'All') { @@ -388,4 +414,18 @@ } } } + + if ($Service -contains 'AipService' -or $Service -contains 'All') { + Write-Host "`nConnecting to Azure Information Protection" -ForegroundColor Yellow + Write-PSFMessage 'Connecting to Azure Information Protection' + + try { + Connect-AipService -ErrorAction Stop + Write-Verbose "Successfully connected to Azure Information Protection." + } + catch { + Write-Host "`nFailed to connect to Azure Information Protection: $_" -ForegroundColor Red + Write-PSFMessage "Failed to connect to Azure Information Protection: $_" -Level Error + } + } } diff --git a/src/powershell/tests/Test-Assessment.35011.ps1 b/src/powershell/tests/Test-Assessment.35011.ps1 new file mode 100644 index 000000000..c6a5591ea --- /dev/null +++ b/src/powershell/tests/Test-Assessment.35011.ps1 @@ -0,0 +1,144 @@ +<# +.SYNOPSIS + Azure Information Protection (AIP) Super User Feature Configuration + +.DESCRIPTION + Evaluates whether the Azure Information Protection (AIP) super user feature is enabled and properly configured with designated super users. The super user feature allows specified service accounts or administrators to decrypt rights-managed content for auditing, search, and compliance purposes. + + The cmdlets require the AipService module (v3.0+) which is only supported on Windows PowerShell 5.1. A PowerShell 7 subprocess workaround is automatically employed if running under PowerShell Core. + +.NOTES + Test ID: 35011 + Pillar: Data + Risk Level: High +#> + +function Test-Assessment-35011 { + [ZtTest( + Category = 'Azure Information Protection', + ImplementationCost = 'Medium', + MinimumLicense = ('Microsoft 365 E5'), + Pillar = 'Data', + RiskLevel = 'High', + SfiPillar = '', + TenantType = ('Workforce','External'), + TestId = 35011, + Title = 'Azure Information Protection (AIP) Super User Feature', + UserImpact = 'Low' + )] + [CmdletBinding()] + param() + + #region Data Collection + Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose + + $activity = 'Checking Azure Information Protection Super User Configuration' + Write-ZtProgress -Activity $activity -Status 'Querying AIP super user settings' + + $superUserFeatureEnabled = $null + $superUsers = @() + $errorMsg = $null + + try { + # Note: AipService must be authenticated in Connect-ZtAssessment first + # This test only performs queries against the authenticated service + + # Query Q1: Check if super user feature is enabled + $superUserFeatureEnabled = Get-AipServiceSuperUserFeature -ErrorAction Stop + + # Query Q2: Get list of configured super users + $superUsers = Get-AipServiceSuperUser -ErrorAction Stop + } + catch { + $errorMsg = $_ + Write-PSFMessage "Error querying AIP Super User configuration: $_" -Level Error + } + #endregion Data Collection + + #region Assessment Logic + $passed = $false + $investigateFlag = $false + + if ($errorMsg) { + $investigateFlag = $true + } + else { + # Evaluation logic: + # 1. If feature is disabled, test fails + if ($superUserFeatureEnabled -eq $false) { + $passed = $false + } + # 2. If feature is enabled, check if at least one super user is configured + elseif ($superUserFeatureEnabled -eq $true) { + $superUserCount = if ($superUsers) { @($superUsers).Count } else { 0 } + + if ($superUserCount -ge 1) { + $passed = $true + } + else { + $passed = $false + } + } + } + #endregion Assessment Logic + + #region Report Generation + $testResultMarkdown = "" + + if ($investigateFlag) { + $testResultMarkdown = "⚠️ Unable to determine AIP super user configuration due to permissions or connection issues.`n`n" + $testResultMarkdown += "**Error Details:**`n" + $testResultMarkdown += "* $errorMsg`n`n" + $testResultMarkdown += "**Possible Causes:**`n" + $testResultMarkdown += "* AipService module not installed (requires v3.0+)`n" + $testResultMarkdown += "* Not connected to AIP service`n" + $testResultMarkdown += "* Insufficient permissions to query AIP configuration`n" + } + else { + if ($passed) { + $testResultMarkdown = "✅ Super user feature is enabled with at least one member configured.`n`n" + } + else { + if ($superUserFeatureEnabled -eq $true) { + $testResultMarkdown = "❌ Super user feature is enabled BUT no members are configured.`n`n" + } + else { + $testResultMarkdown = "❌ Super user feature is DISABLED.`n`n" + } + } + + $testResultMarkdown += "### Azure Information Protection Super User Configuration`n`n" + $testResultMarkdown += "**Feature Status:**`n" + + $featureStatus = if ($superUserFeatureEnabled) { "Enabled" } else { "Disabled" } + $testResultMarkdown += "* Super User Feature: $featureStatus`n`n" + + if ($superUserFeatureEnabled) { + $superUserCount = if ($superUsers) { @($superUsers).Count } else { 0 } + $testResultMarkdown += "**Super Users Configured: $superUserCount**`n`n" + + if ($superUserCount -gt 0) { + $testResultMarkdown += "| Email Address / Service Principal ID | Account Type |`n" + $testResultMarkdown += "| :--- | :--- |`n" + + foreach ($superUser in $superUsers) { + $accountType = if ($superUser -like '*-*-*-*-*') { "Service Principal" } else { "User" } + $testResultMarkdown += "| $superUser | $accountType |`n" + } + + $testResultMarkdown += "`n" + } + } + + $testResultMarkdown += "**Note:** Super user configuration is not available through the Azure portal and must be managed via PowerShell using the AipService module.`n" + } + #endregion Report Generation + + $testResultDetail = @{ + TestId = '35011' + Title = 'Azure Information Protection (AIP) Super User Feature' + Status = $passed + Result = $testResultMarkdown + } + Add-ZtTestResultDetail @testResultDetail +} From d313f2586c5179bf07288291512bad0000e519a1 Mon Sep 17 00:00:00 2001 From: Komal Date: Wed, 7 Jan 2026 13:45:19 +0530 Subject: [PATCH 48/49] add md file --- src/powershell/tests/Test-Assessment.35011.md | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/powershell/tests/Test-Assessment.35011.md diff --git a/src/powershell/tests/Test-Assessment.35011.md b/src/powershell/tests/Test-Assessment.35011.md new file mode 100644 index 000000000..92b9d8f5a --- /dev/null +++ b/src/powershell/tests/Test-Assessment.35011.md @@ -0,0 +1,27 @@ +The super user feature in Azure Information Protection grants designated accounts the ability to decrypt all content protected by the organization's Rights Management service, regardless of the encryption permissions originally assigned. Super users can access encrypted documents even when they are not explicitly granted permissions by the content owner, enabling scenarios such as eDiscovery, data recovery, compliance investigations, and migration from encrypted content. + +Without super user configuration, organizations risk data loss when encryption keys become inaccessible, employees leave without transferring ownership of critical encrypted files, or legal holds require access to protected content where the original rights holders cannot be reached. The super user feature must be explicitly enabled and membership must be carefully controlled—typically limited to service accounts used by compliance tools, backup systems, or eDiscovery platforms rather than individual user accounts. Failure to configure super users creates operational risk where encrypted content becomes permanently inaccessible, while overly broad super user membership creates security risk where unauthorized accounts gain unrestricted access to all protected content. + +**Remediation action** + +To configure super users: + +Connect to Azure Information Protection PowerShell: Connect-AipService +Enable the super user feature: Enable-AipServiceSuperUserFeature +Add super users (service accounts recommended): +For user accounts: Add-AipServiceSuperUser -EmailAddress "serviceaccount@contoso.com" +For service principals: Add-AipServiceSuperUser -ServicePrincipalId "service-principal-id" +Verify configuration: Get-AipServiceSuperUser + +Best practices: + +- Limit super user membership to dedicated service accounts +- Use service principals for automated tools (eDiscovery, backup) +- Avoid assigning super user to individual employee accounts +- Audit super user access regularly +- Document business justification for each super user account + +[Configure super users for Azure Information Protection Enable-AipServiceSuperUserFeature Add-AipServiceSuperUser](https://learn.microsoft.com/en-us/powershell/module/aipservice/enable-aipservicesuperuserfeature) + + +%TestResult% From 929c481a2f1996a385e3586b1f9002c04b54e367 Mon Sep 17 00:00:00 2001 From: Komal Date: Thu, 8 Jan 2026 12:10:49 +0530 Subject: [PATCH 49/49] fix invetigate status and md file --- src/powershell/tests/Test-Assessment.35011.md | 16 ++++--- .../tests/Test-Assessment.35011.ps1 | 44 +++++++++++-------- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.35011.md b/src/powershell/tests/Test-Assessment.35011.md index 92b9d8f5a..614ac023d 100644 --- a/src/powershell/tests/Test-Assessment.35011.md +++ b/src/powershell/tests/Test-Assessment.35011.md @@ -6,12 +6,12 @@ Without super user configuration, organizations risk data loss when encryption k To configure super users: -Connect to Azure Information Protection PowerShell: Connect-AipService -Enable the super user feature: Enable-AipServiceSuperUserFeature -Add super users (service accounts recommended): -For user accounts: Add-AipServiceSuperUser -EmailAddress "serviceaccount@contoso.com" -For service principals: Add-AipServiceSuperUser -ServicePrincipalId "service-principal-id" -Verify configuration: Get-AipServiceSuperUser +1. Connect to Azure Information Protection PowerShell: `Connect-AipService` +2. Enable the super user feature: `Enable-AipServiceSuperUserFeature` +3. Add super users (service accounts recommended): + - For user accounts: `Add-AipServiceSuperUser -EmailAddress "serviceaccount@contoso.com"` + - For service principals: `Add-AipServiceSuperUser -ServicePrincipalId "service-principal-id"` +4. Verify configuration: `Get-AipServiceSuperUser` Best practices: @@ -21,7 +21,9 @@ Best practices: - Audit super user access regularly - Document business justification for each super user account -[Configure super users for Azure Information Protection Enable-AipServiceSuperUserFeature Add-AipServiceSuperUser](https://learn.microsoft.com/en-us/powershell/module/aipservice/enable-aipservicesuperuserfeature) +- [Configure super users for Azure Information Protection](https://learn.microsoft.com/en-us/purview/encryption-super-users) +- [Enable-AipServiceSuperUserFeature](https://learn.microsoft.com/en-us/powershell/module/aipservice/enable-aipservicesuperuserfeature) +- [Add-AipServiceSuperUser](https://learn.microsoft.com/en-us/powershell/module/aipservice/add-aipservicesuperuser) %TestResult% diff --git a/src/powershell/tests/Test-Assessment.35011.ps1 b/src/powershell/tests/Test-Assessment.35011.ps1 index c6a5591ea..28c397c26 100644 --- a/src/powershell/tests/Test-Assessment.35011.ps1 +++ b/src/powershell/tests/Test-Assessment.35011.ps1 @@ -84,15 +84,10 @@ function Test-Assessment-35011 { #region Report Generation $testResultMarkdown = "" + $mdInfo = "" if ($investigateFlag) { $testResultMarkdown = "⚠️ Unable to determine AIP super user configuration due to permissions or connection issues.`n`n" - $testResultMarkdown += "**Error Details:**`n" - $testResultMarkdown += "* $errorMsg`n`n" - $testResultMarkdown += "**Possible Causes:**`n" - $testResultMarkdown += "* AipService module not installed (requires v3.0+)`n" - $testResultMarkdown += "* Not connected to AIP service`n" - $testResultMarkdown += "* Insufficient permissions to query AIP configuration`n" } else { if ($passed) { @@ -107,38 +102,51 @@ function Test-Assessment-35011 { } } - $testResultMarkdown += "### Azure Information Protection Super User Configuration`n`n" - $testResultMarkdown += "**Feature Status:**`n" + # Build detailed information section + $mdInfo = "## Azure Information Protection Super User Configuration`n`n" $featureStatus = if ($superUserFeatureEnabled) { "Enabled" } else { "Disabled" } - $testResultMarkdown += "* Super User Feature: $featureStatus`n`n" + $mdInfo += "**Super User Feature: $featureStatus**`n`n" if ($superUserFeatureEnabled) { $superUserCount = if ($superUsers) { @($superUsers).Count } else { 0 } - $testResultMarkdown += "**Super Users Configured: $superUserCount**`n`n" + $mdInfo += "**Super Users Configured: $superUserCount**`n`n" if ($superUserCount -gt 0) { - $testResultMarkdown += "| Email Address / Service Principal ID | Account Type |`n" - $testResultMarkdown += "| :--- | :--- |`n" + $mdInfo += "| Email Address / Service Principal ID | Account Type |`n" + $mdInfo += "| :--- | :--- |`n" foreach ($superUser in $superUsers) { $accountType = if ($superUser -like '*-*-*-*-*') { "Service Principal" } else { "User" } - $testResultMarkdown += "| $superUser | $accountType |`n" + $mdInfo += "| $superUser | $accountType |`n" } - $testResultMarkdown += "`n" + $mdInfo += "`n" } } - $testResultMarkdown += "**Note:** Super user configuration is not available through the Azure portal and must be managed via PowerShell using the AipService module.`n" + $mdInfo += "**Note:** Super user configuration is not available through the Azure portal and must be managed via PowerShell using the AipService module.`n" + + # Add mdInfo to the main markdown if there's content + if ($mdInfo) { + $testResultMarkdown += "%TestResult%" + } } #endregion Report Generation - $testResultDetail = @{ + # Replace placeholder with actual detailed info + if ($mdInfo) { + $testResultMarkdown = $testResultMarkdown -replace "%TestResult%", $mdInfo + } + + $params = @{ TestId = '35011' - Title = 'Azure Information Protection (AIP) Super User Feature' Status = $passed Result = $testResultMarkdown } - Add-ZtTestResultDetail @testResultDetail + # Add investigate status if needed + if ($investigateFlag -eq $true) { + $params.CustomStatus = 'Investigate' + } + Add-ZtTestResultDetail @params }