Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
213 changes: 213 additions & 0 deletions code-tests/test-assessments/Test-Assessment.35001.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
Describe "Test-Assessment-35001" {
BeforeAll {
$here = $PSScriptRoot
$srcRoot = Join-Path $here "../../src/powershell"

# Mock external module dependencies
if (-not (Get-Command Write-PSFMessage -ErrorAction SilentlyContinue)) {
function Write-PSFMessage {
}
}

# Load the class
$classPath = Join-Path $srcRoot "classes/ZtTest.ps1"
if (-not ("ZtTest" -as [type])) {
. $classPath
}

# Load the SUT
$sut = Join-Path $srcRoot "tests/Test-Assessment.35001.ps1"
. $sut

# Setup output file
$script:outputFile = Join-Path $here "../TestResults/Report-Test-Assessment.35001.md"
$outputDir = Split-Path $script:outputFile
if (-not (Test-Path $outputDir)) {
New-Item -ItemType Directory -Path $outputDir | Out-Null
}
"# Test Results for 35001`n" | Set-Content $script:outputFile
}

BeforeEach {
Mock Write-PSFMessage {}
Mock Write-ZtProgress {}
Mock Get-SafeMarkdown { param($Text) return $Text }
}

Context "When no policies exist" {
It "Should pass" {
Mock Get-ZtConditionalAccessPolicy { return @() }

Mock Add-ZtTestResultDetail {
param($TestId, $Title, $Status, $Result)
"## Scenario: No policies`n`n$Result`n" | Add-Content $script:outputFile
}

Test-Assessment-35001

Should -Invoke Add-ZtTestResultDetail -ParameterFilter {
$Status -eq $true -and $Result -match "Microsoft Rights Management Service \(RMS\) is excluded"
}
}
}

Context "When policies exist but RMS is excluded" {
It "Should pass when 'All' apps included but RMS excluded" {
Mock Get-ZtConditionalAccessPolicy {
return @(
[PSCustomObject]@{
id = "policy-1"
displayName = "Policy 1"
state = "enabled"
conditions = [PSCustomObject]@{
applications = [PSCustomObject]@{
includeApplications = @("All")
excludeApplications = @("00000012-0000-0000-c000-000000000000")
}
}
}
)
}

Mock Add-ZtTestResultDetail {
param($TestId, $Title, $Status, $Result)
"## Scenario: All apps included, RMS excluded`n`n$Result`n" | Add-Content $script:outputFile
}

Test-Assessment-35001

Should -Invoke Add-ZtTestResultDetail -ParameterFilter {
$Status -eq $true
}
}
}

Context "When policies block RMS" {
It "Should fail when 'All' apps included and RMS NOT excluded" {
Mock Get-ZtConditionalAccessPolicy {
return @(
[PSCustomObject]@{
id = "policy-block-all"
displayName = "Block All Policy"
state = "enabled"
conditions = [PSCustomObject]@{
applications = [PSCustomObject]@{
includeApplications = @("All")
excludeApplications = @("some-other-app-id")
}
}
grantControls = [PSCustomObject]@{
builtInControls = @("mfa")
}
sessionControls = [PSCustomObject]@{
signInFrequency = [PSCustomObject]@{
isEnabled = $true
value = 4
type = "hours"
}
}
}
)
}

$script:capturedResult = $null
Mock Add-ZtTestResultDetail {
param($TestId, $Title, $Status, $Result)
$script:capturedResult = $Result
"## Scenario: Block All Policy`n`n$Result`n" | Add-Content $script:outputFile
}

Test-Assessment-35001

Should -Invoke Add-ZtTestResultDetail -ParameterFilter {
$Status -eq $false
}
$script:capturedResult | Should -Match "Block All Policy"
$script:capturedResult | Should -Match "mfa"
$script:capturedResult | Should -Match "Sign-in Frequency"
}

It "Should fail when RMS explicitly included and NOT excluded" {
Mock Get-ZtConditionalAccessPolicy {
return @(
[PSCustomObject]@{
id = "policy-block-rms"
displayName = "Block RMS Policy"
state = "enabled"
conditions = [PSCustomObject]@{
applications = [PSCustomObject]@{
includeApplications = @("00000012-0000-0000-c000-000000000000")
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent indentation: this line has an extra space compared to other Mock statements in the file. The Mock keyword should align with other It block statements at 12 spaces indentation.

Copilot uses AI. Check for mistakes.
excludeApplications = @()
}
}
}
)
}

$script:capturedResult = $null
Mock Add-ZtTestResultDetail {
param($TestId, $Title, $Status, $Result)
$script:capturedResult = $Result
"## Scenario: Block RMS Policy`n`n$Result`n" | Add-Content $script:outputFile
}

Test-Assessment-35001

Should -Invoke Add-ZtTestResultDetail -ParameterFilter {
$Status -eq $false
}
$script:capturedResult | Should -Match "None"
}
}

Context "Error Handling" {
It "Should handle errors gracefully" {
Mock Get-ZtConditionalAccessPolicy { throw "Graph API Error" }

$script:capturedResult = $null
Mock Add-ZtTestResultDetail {
param($TestId, $Title, $Status, $Result)
$script:capturedResult = $Result
"## Scenario: Error Handling`n`n$Result`n" | Add-Content $script:outputFile
}

Test-Assessment-35001

Should -Invoke Add-ZtTestResultDetail -ParameterFilter {
$Status -eq $false
}
$script:capturedResult | Should -Match "Unable to determine RMS exclusion status"
}
}

Context "When policies are disabled" {
It "Should pass when a blocking policy is disabled" {
Mock Get-ZtConditionalAccessPolicy {
return @(
[PSCustomObject]@{
id = "policy-disabled-block"
displayName = "Disabled Block Policy"
state = "disabled"
conditions = [PSCustomObject]@{
applications = [PSCustomObject]@{
includeApplications = @("00000012-0000-0000-c000-000000000000")
excludeApplications = @()
}
}
}
)
}

Mock Add-ZtTestResultDetail {
param($TestId, $Title, $Status, $Result)
"## Scenario: Disabled Policy`n`n$Result`n" | Add-Content $script:outputFile
}

Test-Assessment-35001

Should -Invoke Add-ZtTestResultDetail -ParameterFilter {
$Status -eq $true
}
}
}
}
19 changes: 19 additions & 0 deletions src/powershell/tests/Test-Assessment.35001.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Microsoft Rights Management Service (RMS) is the protection technology that enforces encryption for sensitivity labels and information protection policies. When users access encrypted content, their applications must authenticate to the RMS service (App ID: `00000012-0000-0000-c000-000000000000`) to decrypt the content. If Conditional Access policies incorrectly block or restrict this authentication - for example, by requiring multi-factor authentication (MFA), device compliance, or specific network locations - users will be unable to open encrypted emails, documents, or files protected by sensitivity labels.
This is most notable when trying to collaborate on MIP protected content from an external tenant to the source tenant.
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The phrase "collaborate on MIP protected content from an external tenant to the source tenant" is unclear. Consider revising to "when external users try to collaborate on MIP-protected content with users in the source tenant" for better clarity.

Suggested change
This is most notable when trying to collaborate on MIP protected content from an external tenant to the source tenant.
This is most notable when external users try to collaborate on MIP-protected content with users in the source tenant.

Copilot uses AI. Check for mistakes.
The RMS service should be explicitly excluded from Conditional Access policies that enforce authentication controls, as the application itself is handling the decryption and the user has already authenticated through their primary client application. Blocking RMS authentication prevents the decryption process and breaks information protection workflows across Microsoft 365 services including Outlook, Word, Excel, PowerPoint, Teams, and SharePoint.

**Remediation action**

To exclude RMS from Conditional Access policies:
1. Navigate to [Microsoft Entra admin center > Entra ID > Conditional Access > Policies](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/Policies)
2. Select the policy that is blocking RMS
3. Under Target resources > All resources (formerly 'All cloud apps')
4. Under Exclude, select 'Select resources' and add "Microsoft Rights Management Services" (App ID: `00000012-0000-0000-c000-000000000000`)
5. Save the policy

- [Microsoft Entra configuration for Azure Information Protection](https://learn.microsoft.com/purview/encryption-azure-ad-configuration)
- [Conditional Access policies and encrypted documents](https://learn.microsoft.com/purview/encryption-azure-ad-configuration#conditional-access-policies-and-encrypted-documents)
- [Conditional Access: Cloud apps, actions, and authentication context](https://learn.microsoft.com/entra/identity/conditional-access/concept-conditional-access-cloud-apps)

<!--- Results --->
%TestResult%
145 changes: 145 additions & 0 deletions src/powershell/tests/Test-Assessment.35001.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<#
.SYNOPSIS
Conditional Access RMS Exclusions
#>

function Test-Assessment-35001 {
[ZtTest(
Category = 'Entra',
ImplementationCost = 'Low',
MinimumLicense = ('Microsoft 365 E5'),
Pillar = 'Data',
RiskLevel = 'High',
SfiPillar = '',
TenantType = ('Workforce','External'),
TestId = 35001,
Title = 'Conditional Access RMS Exclusions',
UserImpact = 'Low'
)]
[CmdletBinding()]
param()

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

$activity = 'Checking Conditional Access RMS Exclusions'
Write-ZtProgress -Activity $activity -Status 'Getting Conditional Access policies'
Comment on lines +22 to +26
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test specifies 'MIP_P1' as the MinimumLicense but doesn't include a license check at the beginning of the function. Other tests in the codebase that require specific licenses use Get-ZtLicense to check for the license and skip execution if not present. However, Get-ZtLicense currently only supports 'EntraIDP1', 'EntraIDP2', 'EntraIDGovernance', 'EntraWorkloadID', and 'Intune' - it does not support 'MIP_P1'.

You should either:

  1. Add support for 'MIP_P1' in the Get-ZtLicense function and add a license check at the beginning of this test
  2. Use an alternative license (like EntraIDP1 or EntraIDP2) that includes MIP capabilities, or
  3. If no license check is needed, consider updating the MinimumLicense attribute to reflect this

Copilot uses AI. Check for mistakes.

$rmsAppId = '00000012-0000-0000-c000-000000000000'
$blockingPolicies = @()
$policies = @()
$errorMsg = $null

try {
# Query: Get all enabled Conditional Access policies
$policies = Get-ZtConditionalAccessPolicy | Where-Object { $_.state -eq 'enabled' }
}
catch {
$errorMsg = $_
Write-PSFMessage "Error querying Conditional Access policies: $_" -Level Error
}
#endregion Data Collection

#region Assessment Logic
if ($errorMsg) {
$passed = $false
}
else {
foreach ($policy in $policies) {
$includedApps = $policy.conditions.applications.includeApplications
$excludedApps = $policy.conditions.applications.excludeApplications

$isRmsIncluded = ($includedApps -contains 'All') -or ($includedApps -contains $rmsAppId)
$isRmsExcluded = $excludedApps -contains $rmsAppId

if ($isRmsIncluded -and -not $isRmsExcluded) {
$blockingPolicies += $policy
}
}

$passed = $blockingPolicies.Count -eq 0
}
#endregion Assessment Logic

#region Report Generation
if ($errorMsg) {
$testResultMarkdown = "❌ Unable to determine RMS exclusion status due to error: $errorMsg"
}
elseif ($passed) {
$testResultMarkdown = "✅ Microsoft Rights Management Service (RMS) is excluded from Conditional Access policies that enforce authentication controls."
}
else {
$testResultMarkdown = "❌ Microsoft Rights Management Service (RMS) is blocked or restricted by one or more Conditional Access policies.`n`n"
$testResultMarkdown += "**Policies Affecting RMS:**`n`n"
$testResultMarkdown += "| Policy Name | State | RMS Targeted | RMS Excluded | Grant Controls | Session Controls |`n"
$testResultMarkdown += "| :--- | :--- | :--- | :--- | :--- | :--- |`n"

foreach ($policy in $blockingPolicies) {
$policyLink = "https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/PolicyBlade/policyId/$($policy.id)"

# Grant Controls
$grantControls = @()
if ($policy.grantControls) {
if ($policy.grantControls.builtInControls) { $grantControls += $policy.grantControls.builtInControls }
if ($policy.grantControls.termsOfUse) { $grantControls += "Terms of Use" }
Comment on lines +83 to +84
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition checks if the policy has grantControls before accessing its properties, but does not check if termsOfUse exists before accessing it. While this may not cause an error if termsOfUse is null or undefined in PowerShell, it's inconsistent with the defensive pattern used for builtInControls. Consider adding a null check before accessing policy.grantControls.termsOfUse to maintain consistency.

Suggested change
if ($policy.grantControls.builtInControls) { $grantControls += $policy.grantControls.builtInControls }
if ($policy.grantControls.termsOfUse) { $grantControls += "Terms of Use" }
$grantControlsObj = $policy.grantControls
if ($grantControlsObj.builtInControls) { $grantControls += $grantControlsObj.builtInControls }
if ($grantControlsObj.PSObject.Properties['termsOfUse'] -and $grantControlsObj.termsOfUse) { $grantControls += "Terms of Use" }

Copilot uses AI. Check for mistakes.
}
$grantDisplay = if ($grantControls.Count -gt 0) { $grantControls -join ', ' } else { 'None' }

# Session Controls
$sessionControls = @()
if ($policy.sessionControls) {
foreach ($prop in $policy.sessionControls.PSObject.Properties) {
$name = $prop.Name
$value = $prop.Value

if ($null -eq $value) { continue }

$isSet = $false
if ($value -is [bool]) {
$isSet = $value
}
else {
if ($value.PSObject.Properties.Match('isEnabled')) {
$isSet = $value.isEnabled
}
else {
$isSet = $true
}
}

if ($isSet) {
$displayName = $name -replace '([a-z])([A-Z])', '$1 $2'
$displayName = $displayName.Substring(0,1).ToUpper() + $displayName.Substring(1)
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Substring operation on line 112 could fail if the displayName string is empty after the regex replacement. While unlikely in practice, consider adding a guard check to ensure the string has at least one character before attempting to access index 0 and calling Substring.

Suggested change
$displayName = $displayName.Substring(0,1).ToUpper() + $displayName.Substring(1)
if (-not [string]::IsNullOrEmpty($displayName)) {
$displayName = $displayName.Substring(0,1).ToUpper() + $displayName.Substring(1)
}

Copilot uses AI. Check for mistakes.

switch ($name) {
'disableResilienceDefaults' { $displayName = 'Disable Resilience Defaults' }
'cloudAppSecurity' { $displayName = 'Cloud App Security' }
'signInFrequency' { $displayName = 'Sign-in Frequency' }
'persistentBrowser' { $displayName = 'Persistent Browser' }
'continuousAccessEvaluation' { $displayName = 'Customize Continuous Access Evaluation' }
'globalSecureAccessFilteringProfile' { $displayName = 'Global Secure Access Security Profile' }
'secureSignInSession' { $displayName = 'Secure Sign-in Session' }
'applicationEnforcedRestrictions' { $displayName = 'App Enforced Restrictions' }
'networkAccessSecurity' { $displayName = 'Network Access Security' }
}
$sessionControls += $displayName
}
}
}
$sessionDisplay = if ($sessionControls.Count -gt 0) { $sessionControls -join ', ' } else { 'None' }

$policyName = Get-SafeMarkdown -Text $policy.displayName

$testResultMarkdown += "| [$policyName]($policyLink) | $($policy.state) | Yes | No | $grantDisplay | $sessionDisplay |`n"
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "RMS Targeted" column always shows "Yes" for all policies in the blocking policies list. While this is technically correct (these policies target RMS), it would be more informative to show whether RMS is targeted via "All" apps or explicitly by its App ID. Consider changing this to show the actual targeting method (e.g., "All apps" or "Explicit").

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "RMS Excluded" column always displays "No" for all entries in the blocking policies table because these policies are in the list specifically because they don't exclude RMS (see the filter logic on lines 52-57). This makes the column redundant. Consider removing the "RMS Excluded" column from the table header (line 74) and the table row (line 133) since it provides no additional information - by definition, all policies in this table have RMS not excluded.

Copilot uses AI. Check for mistakes.
}
}
#endregion Report Generation

$testResultDetail = @{
TestId = '35001'
Title = 'Conditional Access RMS Exclusions'
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'Title' parameter is included in the testResultDetail hashtable, but when looking at other tests in the codebase, the Title is typically derived from the ZtTest attribute and not passed explicitly to Add-ZtTestResultDetail. This duplication may be unnecessary. Consider removing the Title parameter from the hashtable to follow the pattern used by other tests, unless there's a specific reason for this deviation.

Suggested change
Title = 'Conditional Access RMS Exclusions'

Copilot uses AI. Check for mistakes.
Status = $passed
Result = $testResultMarkdown
}
Add-ZtTestResultDetail @testResultDetail
}