-
Notifications
You must be signed in to change notification settings - Fork 121
35001 - Add test for Conditional Access RMS exclusions #719
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8548042
4da6083
d0aa4dc
a5605a2
a41c2ef
db458f0
06f6bd9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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") | ||
| 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 | ||
| } | ||
| } | ||
| } | ||
| } | ||
| 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. | ||||||
|
||||||
| 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. |
| 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
|
||||||||||||
|
|
||||||||||||
| $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
|
||||||||||||
| 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
AI
Jan 1, 2026
There was a problem hiding this comment.
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.
| $displayName = $displayName.Substring(0,1).ToUpper() + $displayName.Substring(1) | |
| if (-not [string]::IsNullOrEmpty($displayName)) { | |
| $displayName = $displayName.Substring(0,1).ToUpper() + $displayName.Substring(1) | |
| } |
Copilot
AI
Jan 1, 2026
There was a problem hiding this comment.
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
AI
Jan 1, 2026
There was a problem hiding this comment.
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
AI
Dec 22, 2025
There was a problem hiding this comment.
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.
| Title = 'Conditional Access RMS Exclusions' |
There was a problem hiding this comment.
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.