Public/Get-IOCrossTenantAccessReport.ps1

function Get-IOCrossTenantAccessReport {
    <#
    .SYNOPSIS
        Summarizes cross-tenant access policy settings and flags overly permissive configurations.
    .EXAMPLE
        Get-IOCrossTenantAccessReport
    .EXAMPLE
        Get-IOCrossTenantAccessReport -ToCsv "cross-tenant.csv"
    #>

    [CmdletBinding()]
    param(
        [string]$ToCsv
    )

    $cmdName = $MyInvocation.MyCommand.Name
    Write-IOLog 'Analyzing cross-tenant access policies...' -Level Info -Component $cmdName

    $results = [System.Collections.Generic.List[PSCustomObject]]::new()

    # ── Default policy ─────────────────────────────────────────────────────
    try {
        $defaultPolicy = Invoke-IOGraphRequest -Uri 'v1.0/policies/crossTenantAccessPolicy/default' -SingleResult -NoPagination
    }
    catch {
        Write-IOLog "Could not retrieve cross-tenant access policy. Ensure Policy.Read.All scope is granted." -Level Warning -Component $cmdName
        return
    }

    if ($defaultPolicy -and $defaultPolicy.Count -gt 0) {
        $dp = $defaultPolicy[0]

        $inboundTrust = 'Not Configured'
        if ($dp.inboundTrust) {
            $trustParts = @()
            if ($dp.inboundTrust.isMfaAccepted) { $trustParts += 'MFA' }
            if ($dp.inboundTrust.isCompliantDeviceAccepted) { $trustParts += 'CompliantDevice' }
            if ($dp.inboundTrust.isHybridAzureADJoinedDeviceAccepted) { $trustParts += 'HybridJoined' }
            $inboundTrust = if ($trustParts.Count -gt 0) { $trustParts -join '; ' } else { 'None' }
        }

        $risks = [System.Collections.Generic.List[string]]::new()
        $b2bIn = if ($dp.PSObject.Properties['b2bCollaborationInbound']) { $dp.b2bCollaborationInbound } else { $null }
        $b2bDC = if ($dp.PSObject.Properties['b2bDirectConnectInbound']) { $dp.b2bDirectConnectInbound } else { $null }
        if ($b2bIn -and $b2bIn.PSObject.Properties['applications'] -and $b2bIn.applications.accessType -eq 'allowed' -and
            $b2bIn.applications.PSObject.Properties['targets'] -and $b2bIn.applications.targets.target -contains 'AllApplications') {
            $risks.Add('InboundB2B_AllApps')
        }
        if ($b2bDC -and $b2bDC.PSObject.Properties['applications'] -and $b2bDC.applications.accessType -eq 'allowed' -and
            $b2bDC.applications.PSObject.Properties['targets'] -and $b2bDC.applications.targets.target -contains 'AllApplications') {
            $risks.Add('InboundDirectConnect_AllApps')
        }

        $results.Add([PSCustomObject]@{
            PartnerTenant     = 'DEFAULT (all tenants)'
            PartnerTenantId   = '*'
            Direction         = 'Default Policy'
            B2BInbound        = if ($b2bIn) { 'Configured' } else { 'Not Configured' }
            B2BOutbound       = if ($dp.PSObject.Properties['b2bCollaborationOutbound'] -and $dp.b2bCollaborationOutbound) { 'Configured' } else { 'Not Configured' }
            DirectConnect     = if ($b2bDC) { 'Configured' } else { 'Not Configured' }
            InboundTrust      = $inboundTrust
            Risks             = if ($risks.Count -gt 0) { $risks -join '; ' } else { 'None' }
            RiskCount         = $risks.Count
        })
    }

    # ── Partner-specific policies ──────────────────────────────────────────
    try {
        $partners = Invoke-IOGraphRequest -Uri 'v1.0/policies/crossTenantAccessPolicy/partners'
    }
    catch {
        $partners = @()
    }

    foreach ($partner in $partners) {
        $risks = [System.Collections.Generic.List[string]]::new()

        $inboundTrust = 'Not Configured'
        if ($partner.inboundTrust) {
            $trustParts = @()
            if ($partner.inboundTrust.isMfaAccepted) { $trustParts += 'MFA' }
            if ($partner.inboundTrust.isCompliantDeviceAccepted) { $trustParts += 'CompliantDevice' }
            if ($partner.inboundTrust.isHybridAzureADJoinedDeviceAccepted) { $trustParts += 'HybridJoined' }
            $inboundTrust = if ($trustParts.Count -gt 0) { $trustParts -join '; ' } else { 'None' }
        }

        # Check for overly permissive inbound
        $pB2bIn = if ($partner.PSObject.Properties['b2bCollaborationInbound']) { $partner.b2bCollaborationInbound } else { $null }
        if ($pB2bIn -and $pB2bIn.PSObject.Properties['applications'] -and $pB2bIn.applications.accessType -eq 'allowed') {
            if ($pB2bIn.applications.PSObject.Properties['targets'] -and $pB2bIn.applications.targets.target -contains 'AllApplications') {
                $risks.Add('B2BInbound_AllApps')
            }
        }

        # Check inbound users scope
        if ($pB2bIn -and $pB2bIn.PSObject.Properties['usersAndGroups'] -and $pB2bIn.usersAndGroups.accessType -eq 'allowed') {
            if ($pB2bIn.usersAndGroups.PSObject.Properties['targets'] -and $pB2bIn.usersAndGroups.targets.target -contains 'AllUsers') {
                $risks.Add('B2BInbound_AllUsers')
            }
        }

        $tenantName = $partner.tenantId
        # Try to resolve tenant name
        if ($partner.PSObject.Properties['identityPartner'] -and $partner.identityPartner) {
            $tenantName = "$($partner.identityPartner) ($($partner.tenantId))"
        }

        $results.Add([PSCustomObject]@{
            PartnerTenant     = $tenantName
            PartnerTenantId   = $partner.tenantId
            Direction         = 'Partner-Specific'
            B2BInbound        = if ($pB2bIn) { 'Configured' } else { 'Inherited' }
            B2BOutbound       = if ($partner.PSObject.Properties['b2bCollaborationOutbound'] -and $partner.b2bCollaborationOutbound) { 'Configured' } else { 'Inherited' }
            DirectConnect     = if ($partner.PSObject.Properties['b2bDirectConnectInbound'] -and $partner.b2bDirectConnectInbound) { 'Configured' } else { 'Inherited' }
            InboundTrust      = $inboundTrust
            Risks             = if ($risks.Count -gt 0) { $risks -join '; ' } else { 'None' }
            RiskCount         = $risks.Count
        })
    }

    $sorted = $results | Sort-Object RiskCount -Descending
    Export-IOResult -Data $sorted -ToCsv $ToCsv -CommandName $cmdName
}