diff --git a/code-tests/test-assessments/Test-Assessment.35002.Tests.ps1 b/code-tests/test-assessments/Test-Assessment.35002.Tests.ps1 new file mode 100644 index 000000000..057dea030 --- /dev/null +++ b/code-tests/test-assessments/Test-Assessment.35002.Tests.ps1 @@ -0,0 +1,337 @@ +Describe "Test-Assessment-35002" { + BeforeAll { + $here = $PSScriptRoot + $srcRoot = Join-Path $here "../../src/powershell" +<# + # Import required module functions + @( + "private/core/Add-ZtTestResultDetail.ps1" + "public/Invoke-ZtGraphRequest.ps1" + "private/core/Write-ZtProgress.ps1" + "private/core/Get-ZtTestStatus.ps1" + "private/core/Get-SafeMarkdown.ps1" + ) | ForEach-Object { . (Join-Path $srcRoot $_) } +#> + # 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.35002.ps1" + . $sut + + # Setup output file + $script:outputFile = Join-Path $here "../TestResults/Report-Test-Assessment.35002.md" + $outputDir = Split-Path $script:outputFile + if (-not (Test-Path $outputDir)) { New-Item -ItemType Directory -Path $outputDir | Out-Null } + "# Test Results for 35002`n" | Set-Content $script:outputFile + } + + BeforeEach { + Mock Write-PSFMessage {} + Mock Write-ZtProgress {} + Mock Get-SafeMarkdown { param($Text) return $Text } + $script:defaultPolicyResponse = $null + $script:partnersResponse = @() + } + + Context "When Default Policy allows RMS" { + It "Should pass when Inbound and Outbound allow RMS explicitly" { + $script:defaultPolicyResponse = [PSCustomObject]@{ + b2bCollaborationInbound = [PSCustomObject]@{ + applications = [PSCustomObject]@{ + accessType = "allowed" + targets = @([PSCustomObject]@{ target = "00000012-0000-0000-c000-000000000000" }) + } + } + b2bCollaborationOutbound = [PSCustomObject]@{ + applications = [PSCustomObject]@{ + accessType = "allowed" + targets = @([PSCustomObject]@{ target = "00000012-0000-0000-c000-000000000000" }) + } + } + } + $script:partnersResponse = @() + + Mock Invoke-ZtGraphRequest { + if ($RelativeUri -match "default") { return $script:defaultPolicyResponse } + if ($RelativeUri -match "partners") { return $script:partnersResponse } + } + + Mock Add-ZtTestResultDetail { + param($TestId, $Title, $Status, $Result) + "## Scenario: Default Allowed Explicitly`n`n$Result`n" | Add-Content $script:outputFile + } + + Test-Assessment-35002 + + Should -Invoke Add-ZtTestResultDetail -ParameterFilter { + $Status -eq $true -and $Result -match "RMS application is allowed" + } + } + + It "Should pass when Inbound and Outbound allow All Apps" { + $script:defaultPolicyResponse = [PSCustomObject]@{ + b2bCollaborationInbound = [PSCustomObject]@{ + applications = [PSCustomObject]@{ + accessType = "allowed" + targets = @([PSCustomObject]@{ target = "AllApplications" }) + } + } + b2bCollaborationOutbound = [PSCustomObject]@{ + applications = [PSCustomObject]@{ + accessType = "allowed" + targets = @([PSCustomObject]@{ target = "AllApplications" }) + } + } + } + $script:partnersResponse = @() + + Mock Invoke-ZtGraphRequest { + if ($RelativeUri -match "default") { return $script:defaultPolicyResponse } + if ($RelativeUri -match "partners") { return $script:partnersResponse } + } + + Mock Add-ZtTestResultDetail { + param($TestId, $Title, $Status, $Result) + "## Scenario: Default Allowed All Apps`n`n$Result`n" | Add-Content $script:outputFile + } + + Test-Assessment-35002 + + Should -Invoke Add-ZtTestResultDetail -ParameterFilter { + $Status -eq $true + } + } + + It "Should pass when Inbound and Outbound Block specific apps but NOT RMS (Implicit Allow)" { + $script:defaultPolicyResponse = [PSCustomObject]@{ + b2bCollaborationInbound = [PSCustomObject]@{ + applications = [PSCustomObject]@{ + accessType = "blocked" + targets = @([PSCustomObject]@{ target = "some-other-app-id" }) + } + } + b2bCollaborationOutbound = [PSCustomObject]@{ + applications = [PSCustomObject]@{ + accessType = "blocked" + targets = @([PSCustomObject]@{ target = "some-other-app-id" }) + } + } + } + $script:partnersResponse = @() + + Mock Invoke-ZtGraphRequest { + if ($RelativeUri -match "default") { return $script:defaultPolicyResponse } + if ($RelativeUri -match "partners") { return $script:partnersResponse } + } + + Mock Add-ZtTestResultDetail { + param($TestId, $Title, $Status, $Result) + "## Scenario: Default Implicit Allow`n`n$Result`n" | Add-Content $script:outputFile + } + + Test-Assessment-35002 + + Should -Invoke Add-ZtTestResultDetail -ParameterFilter { + $Status -eq $true + } + } + } + + Context "When Default Policy blocks RMS" { + It "Should fail when Inbound explicitly blocks RMS" { + $script:defaultPolicyResponse = [PSCustomObject]@{ + b2bCollaborationInbound = [PSCustomObject]@{ + applications = [PSCustomObject]@{ + accessType = "blocked" + targets = @([PSCustomObject]@{ target = "00000012-0000-0000-c000-000000000000" }) + } + } + b2bCollaborationOutbound = [PSCustomObject]@{ + applications = [PSCustomObject]@{ + accessType = "allowed" + targets = @([PSCustomObject]@{ target = "AllApplications" }) + } + } + } + $script:partnersResponse = @() + + Mock Invoke-ZtGraphRequest { + if ($RelativeUri -match "default") { return $script:defaultPolicyResponse } + if ($RelativeUri -match "partners") { return $script:partnersResponse } + } + + $script:capturedResult = $null + Mock Add-ZtTestResultDetail { + param($TestId, $Title, $Status, $Result) + $script:capturedResult = $Result + "## Scenario: Default Inbound Blocked Explicitly`n`n$Result`n" | Add-Content $script:outputFile + } + + Test-Assessment-35002 + + Should -Invoke Add-ZtTestResultDetail -ParameterFilter { + $Status -eq $false + } + $script:capturedResult | Should -Match "Blocked \(Explicit\)" + } + + It "Should fail when Inbound allows specific apps but NOT RMS (Implicit Block)" { + $script:defaultPolicyResponse = [PSCustomObject]@{ + b2bCollaborationInbound = [PSCustomObject]@{ + applications = [PSCustomObject]@{ + accessType = "allowed" + targets = @([PSCustomObject]@{ target = "some-other-app-id" }) + } + } + b2bCollaborationOutbound = [PSCustomObject]@{ + applications = [PSCustomObject]@{ + accessType = "allowed" + targets = @([PSCustomObject]@{ target = "AllApplications" }) + } + } + } + $script:partnersResponse = @() + + Mock Invoke-ZtGraphRequest { + if ($RelativeUri -match "default") { return $script:defaultPolicyResponse } + if ($RelativeUri -match "partners") { return $script:partnersResponse } + } + + $script:capturedResult = $null + Mock Add-ZtTestResultDetail { + param($TestId, $Title, $Status, $Result) + $script:capturedResult = $Result + "## Scenario: Default Inbound Blocked Implicitly`n`n$Result`n" | Add-Content $script:outputFile + } + + Test-Assessment-35002 + + Should -Invoke Add-ZtTestResultDetail -ParameterFilter { + $Status -eq $false + } + $script:capturedResult | Should -Match "Blocked \(Implicit\)" + } + } + + Context "When Partner Policies exist" { + It "Should fail if a Partner Policy blocks RMS" { + $script:defaultPolicyResponse = [PSCustomObject]@{ + b2bCollaborationInbound = [PSCustomObject]@{ + applications = [PSCustomObject]@{ + accessType = "allowed" + targets = @([PSCustomObject]@{ target = "AllApplications" }) + } + } + b2bCollaborationOutbound = [PSCustomObject]@{ + applications = [PSCustomObject]@{ + accessType = "allowed" + targets = @([PSCustomObject]@{ target = "AllApplications" }) + } + } + } + $script:partnersResponse = @( + [PSCustomObject]@{ + tenantId = "partner-tenant-id" + b2bCollaborationInbound = [PSCustomObject]@{ + applications = [PSCustomObject]@{ + accessType = "blocked" + targets = @([PSCustomObject]@{ target = "00000012-0000-0000-c000-000000000000" }) + } + } + } + ) + + Mock Invoke-ZtGraphRequest { + if ($RelativeUri -match "default") { return $script:defaultPolicyResponse } + if ($RelativeUri -match "partners") { return $script:partnersResponse } + } + + $script:capturedResult = $null + Mock Add-ZtTestResultDetail { + param($TestId, $Title, $Status, $Result) + $script:capturedResult = $Result + "## Scenario: Partner Blocked`n`n$Result`n" | Add-Content $script:outputFile + } + + Test-Assessment-35002 + + Should -Invoke Add-ZtTestResultDetail -ParameterFilter { + $Status -eq $false + } + $script:capturedResult | Should -Match "Partner \(partner-tenant-id\)" + $script:capturedResult | Should -Match "Blocked \(Explicit\)" + } + + It "Should ignore inherited partner settings" { + $script:defaultPolicyResponse = [PSCustomObject]@{ + b2bCollaborationInbound = [PSCustomObject]@{ + applications = [PSCustomObject]@{ + accessType = "allowed" + targets = @([PSCustomObject]@{ target = "AllApplications" }) + } + } + b2bCollaborationOutbound = [PSCustomObject]@{ + applications = [PSCustomObject]@{ + accessType = "allowed" + targets = @([PSCustomObject]@{ target = "AllApplications" }) + } + } + } + # Partner with null/empty settings implies inheritance + $script:partnersResponse = @( + [PSCustomObject]@{ + tenantId = "partner-tenant-id" + b2bCollaborationInbound = $null + b2bCollaborationOutbound = [PSCustomObject]@{ + applications = $null + } + } + ) + + Mock Invoke-ZtGraphRequest { + if ($RelativeUri -match "default") { return $script:defaultPolicyResponse } + if ($RelativeUri -match "partners") { return $script:partnersResponse } + } + + Mock Add-ZtTestResultDetail { + param($TestId, $Title, $Status, $Result) + "## Scenario: Partner Inherited`n`n$Result`n" | Add-Content $script:outputFile + } + + Test-Assessment-35002 + + Should -Invoke Add-ZtTestResultDetail -ParameterFilter { + $Status -eq $true + } + } + } + + Context "Error Handling" { + It "Should handle Graph API errors" { + Mock Invoke-ZtGraphRequest { 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-35002 + + Should -Invoke Add-ZtTestResultDetail -ParameterFilter { + $Status -eq $false + } + $script:capturedResult | Should -Match "Cross-tenant access policy settings cannot be determined" + } + } +} diff --git a/src/powershell/tests/Test-Assessment.35002.md b/src/powershell/tests/Test-Assessment.35002.md new file mode 100644 index 000000000..a59cafee1 --- /dev/null +++ b/src/powershell/tests/Test-Assessment.35002.md @@ -0,0 +1,18 @@ +Cross-tenant access policies (XTAP) in Microsoft Entra ID control how users in your organization collaborate with external organizations. When users share encrypted content across organizational boundaries or receive encrypted documents from external partners, the Microsoft Rights Management Service (RMS) must authenticate users from both organizations to enforce encryption permissions. If cross-tenant access settings block or restrict the RMS application (App ID: `00000012-0000-0000-c000-000000000000`), users will encounter "Access is blocked by your organization" or "Access is blocked by the organization" error messages when attempting to open encrypted emails or documents from external organizations. This prevents legitimate cross-organizational collaboration on protected content. Organizations should configure both inbound and outbound cross-tenant access settings to explicitly allow the RMS application, ensuring that external users can open encrypted content shared by your organization (inbound) and your users can open encrypted content received from external partners (outbound). Without proper XTAP configuration, encrypted content sharing fails even when users have appropriate permissions assigned through sensitivity label encryption settings. + +**Remediation action** + +To configure cross-tenant access settings to allow RMS: +1. Navigate to [Microsoft Entra admin center > External Identities > Cross-tenant access settings](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/CompanyRelationshipsMenuBlade/~/CrossTenantAccessSettings) +2. Select "Default settings" or a specific organizational setting +3. Under "Inbound access", select "B2B collaboration" +4. Select "Applications" tab +5. Choose "Allow access" and add "Microsoft Rights Management Services" (App ID: `00000012-0000-0000-c000-000000000000`) +6. Repeat for "Outbound access" settings +7. Save changes + +- [Cross-tenant access settings and encrypted content](https://learn.microsoft.com/purview/encryption-azure-ad-configuration#cross-tenant-access-settings-and-encrypted-content) +- [Configure cross-tenant access settings for B2B collaboration](https://learn.microsoft.com/entra/external-id/cross-tenant-access-settings-b2b-collaboration) + + +%TestResult% diff --git a/src/powershell/tests/Test-Assessment.35002.ps1 b/src/powershell/tests/Test-Assessment.35002.ps1 new file mode 100644 index 000000000..3e9cb831f --- /dev/null +++ b/src/powershell/tests/Test-Assessment.35002.ps1 @@ -0,0 +1,210 @@ +<# +.SYNOPSIS + Checks if Microsoft Rights Management Services (RMS) is allowed in Cross-Tenant Access Policies (XTAP). + +.DESCRIPTION + This test verifies that the Microsoft Rights Management Services (RMS) application (App ID: 00000012-0000-0000-c000-000000000000) + is allowed in both Inbound and Outbound Cross-Tenant Access Policies. + It checks the Default policy and any Partner-specific policies. + + RMS is required for decrypting content shared across tenants (e.g., encrypted emails, MIP labels). + Blocking it prevents users from opening protected content. + +.NOTES + Test ID: 35002 + Pillar: Data + Risk Level: High + Graph Scopes: Policy.Read.All, CrossTenantInformation.ReadBasic.All +#> + +function Test-Assessment-35002 { + [ZtTest( + Category = 'Entra', + ImplementationCost = 'Low', + MinimumLicense = ('Microsoft 365 E5'), + Pillar = 'Data', + RiskLevel = 'High', + SfiPillar = '', + TenantType = ('Workforce'), + TestId = 35002, + Title = 'Cross-Tenant Access Policy (XTAP) RMS Inbound/Outbound Settings', + UserImpact = 'Low' + )] + [CmdletBinding()] + param() + + #region Helper Functions + function Get-RmsAccessStatus { + param ( + $Settings + ) + + $rmsAppId = "00000012-0000-0000-c000-000000000000" + $allApps = "AllApplications" + + if ($null -eq $Settings -or $null -eq $Settings.applications) { + return "Inherited" + } + + $accessType = $Settings.applications.accessType + # Handle targets being an array or single object or null + $targets = @() + if ($Settings.applications.targets) { + $targets = $Settings.applications.targets | ForEach-Object { $_.target } + } + + if ($accessType -eq "allowed") { + # In "Allowed" mode, only listed apps are allowed. + if ($targets -contains $allApps -or $targets -contains $rmsAppId) { + return "Allowed" + } + return "Blocked (Implicit)" + } + elseif ($accessType -eq "blocked") { + # In "Blocked" mode, listed apps are blocked. + if ($targets -contains $allApps -or $targets -contains $rmsAppId) { + return "Blocked (Explicit)" + } + return "Allowed (Implicit)" + } + + return "Unknown" + } + #endregion Helper Functions + + #region Data Collection + Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose + + $activity = 'Checking Cross-Tenant Access Policy (XTAP) RMS Settings' + Write-ZtProgress -Activity $activity -Status 'Getting Default Policy' + + $defaultPolicy = $null + $partners = @() + $errorMsg = $null + + try { + # 1. Get Default Policy + $defaultPolicy = Invoke-ZtGraphRequest -RelativeUri 'policies/crossTenantAccessPolicy/default' -ApiVersion v1.0 -ErrorAction Stop + + # 2. Get Partner Policies + Write-ZtProgress -Activity $activity -Status 'Getting Partner Policies' + $partners = Invoke-ZtGraphRequest -RelativeUri 'policies/crossTenantAccessPolicy/partners' -ApiVersion v1.0 -ErrorAction Stop + } + catch { + $errorMsg = $_ + Write-PSFMessage "Error querying Cross-Tenant Access Policies: $_" -Level Error + } + #endregion Data Collection + + #region Assessment Logic + $xtapResults = @() + $hasFailure = $false + + if ($errorMsg) { + $passed = $false + } + else { + # Check Default Inbound + if ($defaultPolicy) { + $inboundStatus = Get-RmsAccessStatus -Settings $defaultPolicy.b2bCollaborationInbound + if ($inboundStatus -notlike "Allowed*") { $hasFailure = $true } + + $xtapResults += [PSCustomObject]@{ + Policy = "Default" + Direction = "Inbound" + Status = $inboundStatus + Details = "B2B Collaboration" + } + + # Check Default Outbound + $outboundStatus = Get-RmsAccessStatus -Settings $defaultPolicy.b2bCollaborationOutbound + if ($outboundStatus -notlike "Allowed*") { $hasFailure = $true } + + $xtapResults += [PSCustomObject]@{ + Policy = "Default" + Direction = "Outbound" + Status = $outboundStatus + Details = "B2B Collaboration" + } + } + + # Check Partners + foreach ($partner in $partners) { + $tenantId = $partner.tenantId + + # Check Inbound + if ($partner.b2bCollaborationInbound) { + $pInboundStatus = Get-RmsAccessStatus -Settings $partner.b2bCollaborationInbound + if ($pInboundStatus -ne "Inherited") { + if ($pInboundStatus -notlike "Allowed*") { $hasFailure = $true } + + $xtapResults += [PSCustomObject]@{ + Policy = "Partner ($tenantId)" + Direction = "Inbound" + Status = $pInboundStatus + Details = "Explicit Override" + } + } + } + + # Check Outbound + if ($partner.b2bCollaborationOutbound) { + $pOutboundStatus = Get-RmsAccessStatus -Settings $partner.b2bCollaborationOutbound + if ($pOutboundStatus -ne "Inherited") { + if ($pOutboundStatus -notlike "Allowed*") { $hasFailure = $true } + + $xtapResults += [PSCustomObject]@{ + Policy = "Partner ($tenantId)" + Direction = "Outbound" + Status = $pOutboundStatus + Details = "Explicit Override" + } + } + } + } + + $passed = -not $hasFailure + } + #endregion Assessment Logic + + #region Report Generation + $rmsAppId = "00000012-0000-0000-c000-000000000000" + + if ($errorMsg) { + $testResultMarkdown = "### Investigate`n`n" + $testResultMarkdown += "Cross-tenant access policy settings cannot be determined or RMS is not explicitly configured.`n`n" + $testResultMarkdown += "Please check the console output for error details." + } + else { + if ($passed) { + $testResultMarkdown = "✅ RMS application is allowed (or not restricted) in cross-tenant access policy settings for both inbound and outbound access.`n`n" + } + else { + $testResultMarkdown = "❌ RMS application is explicitly blocked in cross-tenant access policy inbound or outbound settings.`n`n" + } + + $testResultMarkdown += "### Cross-Tenant Access Policy (XTAP) RMS Settings`n`n" + $testResultMarkdown += "| Policy | Direction | Status | Details |`n" + $testResultMarkdown += "|:---|:---|:---|:---|`n" + + foreach ($result in $xtapResults) { + $icon = if ($result.Status -like "Allowed*") { "✅" } else { "❌" } + $testResultMarkdown += "| $($result.Policy) | $($result.Direction) | $icon $($result.Status) | $($result.Details) |`n" + } + $testResultMarkdown += "`n`n" + + if (-not $passed) { + $testResultMarkdown += "⚠️ **Risk:** Blocking RMS prevents users from opening encrypted content (emails, documents) shared between tenants.`n" + $testResultMarkdown += "Please review the blocked policies and add 'Microsoft Rights Management Services' (App ID: $rmsAppId) to the allowed applications list.`n" + } + } + #endregion Report Generation + + $testResultDetail = @{ + TestId = '35002' + Title = 'Cross-Tenant Access Policy (XTAP) RMS Inbound/Outbound Settings' + Status = $passed + Result = $testResultMarkdown + } + Add-ZtTestResultDetail @testResultDetail +}