Checks/Teams/MET-Teams014-CrossTenantAccess.ps1
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'METCheckInfo', Justification = 'Check metadata. Read from the AST by Get-METCheck and never executed.')] param() $METCheckInfo = @{ Name = 'Cross-Tenant Guest & External Collaboration Restrictions' Severity = 'Medium' Description = 'Checks the Microsoft Graph cross-tenant access default policy and authorization policy for guest invitation and external collaboration settings.' RequiresModule = @('Microsoft.Graph') } $defaultPolicy = $null $authPolicy = $null # This is the first MET check with a direct Microsoft Graph dependency (every prior Graph # call site lives in the private Expand-METGroupMembership helper, not a check body). There # is no Exchange Online or native Teams-module equivalent for Entra's cross-tenant access # default policy, so per CLAUDE.md's bar for a direct Graph dependency this is justified - # but it must still degrade non-fatally to NotApplicable rather than aborting the run, # mirroring Expand-METGroupMembership's try/catch pattern. # # Two distinct "could not run" cases, deliberately reported differently: # 1. Graph was never connected (-SkipGraph, the Microsoft.Graph.Identity.SignIns module # absent, or the known EXO/Graph MSAL conflict at Connect-METSession). This is an # expected, documented degradation - the reason goes in Finding, NOT the Error field, # so a routine -SkipGraph run does not park this check in the report's Error bucket. # 2. Graph is connected but a policy call failed (most often a missing Policy.Read.All # scope). That is an unexpected failure worth surfacing, so the Error field is set. $graphCmdletsAvailable = [bool](Get-Command -Name Get-MgPolicyCrossTenantAccessPolicyDefault -ErrorAction SilentlyContinue) -and [bool](Get-Command -Name Get-MgPolicyAuthorizationPolicy -ErrorAction SilentlyContinue) if (-not $graphCmdletsAvailable) { # Get-MgContext ships in Microsoft.Graph.Authentication, the policy cmdlets in # Microsoft.Graph.Identity.SignIns - a session can be live with only the former installed. $graphConnected = [bool](Get-Command -Name Get-MgContext -ErrorAction SilentlyContinue) -and [bool](Get-MgContext -ErrorAction SilentlyContinue) if ($graphConnected) { New-METCheckResult -CheckId 'MET-Teams014' -Category Teams ` -Name 'Cross-Tenant Guest & External Collaboration Restrictions' ` -Result NotApplicable -Severity Medium ` -AffectedObject 'Cross-Tenant Access Policy' ` -Finding 'Microsoft Graph is connected, but the Microsoft.Graph.Identity.SignIns module that provides the cross-tenant access and authorization policy cmdlets could not be loaded - it is not installed, or is installed at a version that does not match Microsoft.Graph.Authentication - so these policies could not be retrieved. Their settings were not established, so this check is reported as not assessed rather than graded.' ` -Recommendation 'Install the module at the same version as Microsoft.Graph.Authentication - Install-Module Microsoft.Graph.Identity.SignIns -RequiredVersion <Authentication version> -Scope CurrentUser -Force - then start a new PowerShell session and re-run. Compare versions with: Get-Module Microsoft.Graph.* -ListAvailable.' ` -ReferenceUrl 'https://learn.microsoft.com/en-us/graph/api/crosstenantaccesspolicy-get' return } New-METCheckResult -CheckId 'MET-Teams014' -Category Teams ` -Name 'Cross-Tenant Guest & External Collaboration Restrictions' ` -Result NotApplicable -Severity Medium ` -AffectedObject 'Cross-Tenant Access Policy' ` -Finding 'Microsoft Graph was not connected for this run, so the Entra ID cross-tenant access default policy and authorization policy could not be retrieved and this check could not run. This is not a failure - it is skipped whenever Connect-METSession runs with -SkipGraph, without the Microsoft.Graph.Identity.SignIns module installed, or when the Graph connection could not be established.' ` -Recommendation 'To include this check, run Connect-METSession without -SkipGraph, with the Microsoft.Graph.Identity.SignIns module (2.x) installed and the Policy.Read.All scope consented, then re-run.' ` -ReferenceUrl 'https://learn.microsoft.com/en-us/graph/api/crosstenantaccesspolicy-get' return } try { $defaultPolicy = Get-MgPolicyCrossTenantAccessPolicyDefault -ErrorAction Stop $authPolicy = Get-MgPolicyAuthorizationPolicy -ErrorAction Stop } catch { Write-Verbose "Cross-tenant access policy retrieval via Microsoft Graph failed: $_" if ($_.Exception -is [System.Management.Automation.CommandNotFoundException]) { # The cmdlet resolved a moment ago but the call reports it as unknown - treat this # as Graph-not-connected (case 1), not a check error. New-METCheckResult -CheckId 'MET-Teams014' -Category Teams ` -Name 'Cross-Tenant Guest & External Collaboration Restrictions' ` -Result NotApplicable -Severity Medium ` -AffectedObject 'Cross-Tenant Access Policy' ` -Finding 'Microsoft Graph was not connected for this run, so the Entra ID cross-tenant access default policy and authorization policy could not be retrieved and this check could not run. This is not a failure - it is skipped whenever Connect-METSession runs with -SkipGraph, without the Microsoft.Graph.Identity.SignIns module installed, or when the Graph connection could not be established.' ` -Recommendation 'To include this check, run Connect-METSession without -SkipGraph, with the Microsoft.Graph.Identity.SignIns module (2.x) installed and the Policy.Read.All scope consented, then re-run.' ` -ReferenceUrl 'https://learn.microsoft.com/en-us/graph/api/crosstenantaccesspolicy-get' return } New-METCheckResult -CheckId 'MET-Teams014' -Category Teams ` -Name 'Cross-Tenant Guest & External Collaboration Restrictions' ` -Result NotApplicable -Severity Medium ` -AffectedObject 'Cross-Tenant Access Policy' ` -Finding 'Microsoft Graph is connected, but retrieving the Entra ID cross-tenant access default policy or authorization policy failed, so this check could not run. The signed-in identity most likely lacks the Policy.Read.All scope.' ` -Recommendation 'Grant the Policy.Read.All scope to the identity MET connects with, then re-run. In the Entra admin center the same settings are under External Identities > Cross-tenant access settings.' ` -ReferenceUrl 'https://learn.microsoft.com/en-us/graph/api/crosstenantaccesspolicy-get' ` -ErrorMessage $_.ToString() return } $findings = [System.Collections.Generic.List[string]]::new() $evaluated = $false $hasConcern = $false # --- Default cross-tenant access policy: inbound B2B collaboration/direct connect --- if ($null -ne $defaultPolicy) { $isServiceDefaultProp = $defaultPolicy.PSObject.Properties['IsServiceDefault'] if ($isServiceDefaultProp -and $isServiceDefaultProp.Value -eq $true) { $evaluated = $true $hasConcern = $true $findings.Add('The default cross-tenant access policy has not been customized (IsServiceDefault=true) - the tenant is relying on the Microsoft Entra system default, which permits inbound and outbound B2B collaboration with any external Microsoft Entra organization unless explicitly restricted') } foreach ($direction in @('B2BCollaborationInbound', 'B2BCollaborationOutbound', 'B2BDirectConnectInbound', 'B2BDirectConnectOutbound')) { $directionProp = $defaultPolicy.PSObject.Properties[$direction] if (-not $directionProp -or $null -eq $directionProp.Value) { continue } $setting = $directionProp.Value foreach ($scope in @('UsersAndGroups', 'Applications')) { $scopeProp = $setting.PSObject.Properties[$scope] if (-not $scopeProp -or $null -eq $scopeProp.Value) { continue } $scopeValue = $scopeProp.Value $accessTypeProp = $scopeValue.PSObject.Properties['AccessType'] if (-not $accessTypeProp -or $null -eq $accessTypeProp.Value) { continue } $evaluated = $true if ([string]$accessTypeProp.Value -eq 'allowed' -and $direction -like '*Inbound*') { $targetDescription = 'no explicit target restriction' $targetsProp = $scopeValue.PSObject.Properties['Targets'] if ($targetsProp -and $targetsProp.Value) { $targetNames = @($targetsProp.Value | ForEach-Object { $_.Target }) -join ', ' if ($targetNames) { $targetDescription = "targets: $targetNames" } } $hasConcern = $true $findings.Add("$direction ($scope) is set to Allowed ($targetDescription) - external users, groups, or applications from unconfigured/unknown external tenants can access your organization's resources via this path by default") } } } } # --- Authorization policy: who can invite guests --- if ($null -ne $authPolicy) { $allowInvitesProp = $authPolicy.PSObject.Properties['AllowInvitesFrom'] if ($allowInvitesProp -and $allowInvitesProp.Value) { $evaluated = $true if ([string]$allowInvitesProp.Value -eq 'everyone') { $hasConcern = $true $findings.Add("AllowInvitesFrom is set to 'everyone' - any user in the organization, including existing guests, can invite new external guests without administrator review") } } } if (-not $evaluated) { New-METCheckResult -CheckId 'MET-Teams014' -Category Teams ` -Name 'Cross-Tenant Guest & External Collaboration Restrictions' ` -Result Info -Severity Medium ` -AffectedObject 'Cross-Tenant Access Policy' ` -Finding 'Retrieved the default cross-tenant access policy and authorization policy from Microsoft Graph, but could not identify any recognizable settings (IsServiceDefault, B2B collaboration/direct connect inbound-outbound access type, or AllowInvitesFrom) to evaluate a Pass/Fail condition. The returned objects may be from an unexpected module version or shape - manual review is required.' ` -Recommendation 'Review the cross-tenant access default policy and authorization policy manually in the Entra admin center (entra.microsoft.com) > External Identities > Cross-tenant access settings.' ` -ReferenceUrl 'https://learn.microsoft.com/en-us/graph/api/crosstenantaccesspolicy-get' return } if ($hasConcern) { New-METCheckResult -CheckId 'MET-Teams014' -Category Teams ` -Name 'Cross-Tenant Guest & External Collaboration Restrictions' ` -Result Warning -Severity Medium ` -AffectedObject 'Cross-Tenant Access Policy' ` -Finding ($findings -join '; ') ` -Recommendation 'Review and scope the default cross-tenant access policy in the Entra admin center (entra.microsoft.com) > External Identities > Cross-tenant access settings > Default settings. Restrict inbound B2B collaboration/direct connect access to explicit organizations, users, or groups rather than relying on the open system default, and set AllowInvitesFrom to a more restrictive value (e.g. adminsAndGuestInviters) unless broad guest-invite rights are a deliberate business decision. Use Update-MgPolicyCrossTenantAccessPolicyDefault and Update-MgPolicyAuthorizationPolicy to remediate.' ` -ReferenceUrl 'https://learn.microsoft.com/en-us/graph/api/crosstenantaccesspolicy-get' } else { New-METCheckResult -CheckId 'MET-Teams014' -Category Teams ` -Name 'Cross-Tenant Guest & External Collaboration Restrictions' ` -Result Pass -Severity Medium ` -AffectedObject 'Cross-Tenant Access Policy' ` -Finding 'The default cross-tenant access policy is customized and inbound B2B collaboration/direct connect settings and guest-invite rights do not indicate open, unrestricted external collaboration' ` -ReferenceUrl 'https://learn.microsoft.com/en-us/graph/api/crosstenantaccesspolicy-get' } |