diff --git a/code-tests/test-assessments/Test-Assessment.35001.Tests.ps1 b/code-tests/test-assessments/Test-Assessment.35001.Tests.ps1 new file mode 100644 index 000000000..a5b615300 --- /dev/null +++ b/code-tests/test-assessments/Test-Assessment.35001.Tests.ps1 @@ -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 + } + } + } +} diff --git a/src/powershell/tests/Test-Assessment.35001.md b/src/powershell/tests/Test-Assessment.35001.md new file mode 100644 index 000000000..f94439f8f --- /dev/null +++ b/src/powershell/tests/Test-Assessment.35001.md @@ -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. +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) + + +%TestResult% diff --git a/src/powershell/tests/Test-Assessment.35001.ps1 b/src/powershell/tests/Test-Assessment.35001.ps1 new file mode 100644 index 000000000..6f40772ed --- /dev/null +++ b/src/powershell/tests/Test-Assessment.35001.ps1 @@ -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' + + $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" } + } + $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) + + 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" + } + } + #endregion Report Generation + + $testResultDetail = @{ + TestId = '35001' + Title = 'Conditional Access RMS Exclusions' + Status = $passed + Result = $testResultMarkdown + } + Add-ZtTestResultDetail @testResultDetail +}