Src/Private/Get-AbrSPExternalAccess.ps1

function Get-AbrSPExternalAccess {
    <#
    .SYNOPSIS
    Documents SharePoint Online external access controls, device access policies,
    idle session settings, and network location restrictions.
    .NOTES
        Version: 0.1.2
        Author: Pai Wei Sing
    #>

    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory)]
        [string]$TenantId
    )

    begin {
        Write-PScriboMessage -Message "Collecting SharePoint External Access data for $TenantId."
        Show-AbrDebugExecutionTime -Start -TitleMessage 'External Access'
    }

    process {
        Section -Style Heading2 'External Access Controls' {
            Paragraph "The following section documents the access control policies applied to SharePoint Online and OneDrive for Business for tenant $TenantId."
            BlankLine

            # Compliance check variables -- safe defaults
            $ConditionalAccessPolicy   = 'AllowFullAccess'
            $SignOutInactiveUsersAfter = 0
            $BlockDownloadPolicy       = 'Not configured'
            $BlockDownloadLinksFileType = 'None'
            $DefaultSharingLinkType    = 'Unknown'

            try {
                if (-not $script:PnPAvailable) {
                    Paragraph " [!] PnP.PowerShell is not available. Detailed access control settings require PnP.PowerShell."
                } else {
                    $SPTenant = Get-PnPTenant -ErrorAction Stop

                    # Safe enum-to-string: guard every .ToString() against null
                    $ConditionalAccessPolicy = ConvertTo-SPEnumString $SPTenant.ConditionalAccessPolicy 'AllowFullAccess'
                    $DefaultSharingLinkType  = ConvertTo-SPEnumString $SPTenant.DefaultSharingLinkType  'Direct'

                    #region Access Control Summary
                    $AccessObj = [System.Collections.ArrayList]::new()

                    $CALabel = switch ($ConditionalAccessPolicy) {
                        'AllowFullAccess'    { 'Allow full access (unmanaged devices permitted)' }
                        'AllowLimitedAccess' { 'Limited web-only access (no download/print/sync)' }
                        'BlockAccess'        { 'Block access from unmanaged devices' }
                        default              { $ConditionalAccessPolicy }
                    }

                    $accessInObj = [ordered] @{
                        'Unmanaged Device Policy'              = $CALabel
                        'Block Download (All File Types)'      = ($null -ne $SPTenant.BlockDownloadLinksFileType -and
                                                                  "$($SPTenant.BlockDownloadLinksFileType)" -ne 'None')
                        'Sync Client Domain Restriction'       = ($null -ne $SPTenant.AllowedDomainGuidsForSyncApp -and
                                                                  @($SPTenant.AllowedDomainGuidsForSyncApp).Count -gt 0)
                        'Legacy Authentication Protocols'      = $SPTenant.LegacyAuthProtocolsEnabled
                        'Email Attestation Required'           = $SPTenant.EmailAttestationRequired
                        'Email Attestation Re-auth Days'       = if ($SPTenant.EmailAttestationReAuthDays -gt 0) {
                                                                      $SPTenant.EmailAttestationReAuthDays
                                                                  } else { 'Not set' }
                    }
                    $AccessObj.Add([pscustomobject](ConvertTo-HashToYN $accessInObj)) | Out-Null

                    $null = (& {
                        if ($HealthCheck.SharePoint.ExternalAccess) {
                            $null = ($AccessObj | Where-Object { $_.'Unmanaged Device Policy' -like '*unmanaged devices permitted*' } | Set-Style -Style Warning | Out-Null)
                            $null = ($AccessObj | Where-Object { $_.'Legacy Authentication Protocols' -eq 'Yes' } | Set-Style -Style Warning | Out-Null)
                        }
                    })

                    $TableParams = @{ Name = "Access Control Settings - $TenantId"; List = $true; ColumnWidths = 55, 45 }
                    if ($Report.ShowTableCaptions) { $TableParams['Caption'] = "- $($TableParams.Name)" }
                    $AccessObj | Table @TableParams
                    BlankLine
                    #endregion

                    #region Idle Session Sign-Out
                    try {
                        $IdleObj               = [System.Collections.ArrayList]::new()
                        $SignOutInactiveUsersAfter = if ($SPTenant.SignOutInactiveUsersAfter) { $SPTenant.SignOutInactiveUsersAfter } else { 0 }
                        $SignOutEnabled         = ($SignOutInactiveUsersAfter -gt 0)
                        $WarnBeforeSignOut      = if ($SPTenant.WarnBeforeSignOut) { $SPTenant.WarnBeforeSignOut } else { 0 }

                        $idleInObj = [ordered] @{
                            'Idle Session Sign-Out Enabled'  = $SignOutEnabled
                            'Sign Out After (minutes)'       = if ($SignOutInactiveUsersAfter -gt 0) { $SignOutInactiveUsersAfter } else { 'Not configured' }
                            'Warn Before Sign-Out (minutes)' = if ($WarnBeforeSignOut -gt 0)         { $WarnBeforeSignOut         } else { 'Not configured' }
                        }
                        $IdleObj.Add([pscustomobject](ConvertTo-HashToYN $idleInObj)) | Out-Null

                        $null = (& {
                            if ($HealthCheck.SharePoint.ExternalAccess) {
                                $null = ($IdleObj | Where-Object { $_.'Idle Session Sign-Out Enabled' -eq 'No' } | Set-Style -Style Warning | Out-Null)
                            }
                        })

                        $IdleTableParams = @{ Name = "Idle Session Sign-Out - $TenantId"; List = $true; ColumnWidths = 55, 45 }
                        if ($Report.ShowTableCaptions) { $IdleTableParams['Caption'] = "- $($IdleTableParams.Name)" }
                        $IdleObj | Table @IdleTableParams
                        BlankLine
                    } catch {
                        Write-AbrDebugLog "Could not retrieve idle session settings: $($_.Exception.Message)" 'WARN' 'EXTERNAL-ACCESS'
                    }
                    #endregion

                    #region Network Location (IP Restriction)
                    try {
                        $IPRanges = $SPTenant.IPAddressAllowList
                        if ($IPRanges) {
                            $NetObj    = [System.Collections.ArrayList]::new()
                            $netInObj  = [ordered] @{
                                'IP Address Restriction Enabled' = $true
                                'Enforcement Mode'               = if ($SPTenant.IPAddressEnforcement) { 'Enforced' } else { 'Audit only' }
                                'Allowed IP Ranges'              = ($IPRanges -join ', ')
                            }
                            $NetObj.Add([pscustomobject](ConvertTo-HashToYN $netInObj)) | Out-Null
                            $NetTableParams = @{ Name = "Network Location (IP Restriction) - $TenantId"; List = $true; ColumnWidths = 40, 60 }
                            if ($Report.ShowTableCaptions) { $NetTableParams['Caption'] = "- $($NetTableParams.Name)" }
                            $NetObj | Table @NetTableParams
                        } else {
                            Paragraph "Network location (IP range) restriction: Not configured. All IP addresses are permitted."
                        }
                        BlankLine
                    } catch {
                        Write-AbrDebugLog "Could not retrieve network location settings: $($_.Exception.Message)" 'WARN' 'EXTERNAL-ACCESS'
                    }
                    #endregion

                    $BlockDownloadLinksFileType = ConvertTo-SPEnumString $SPTenant.BlockDownloadLinksFileType 'None'
                    $BlockDownloadPolicy = if ($ConditionalAccessPolicy -ne 'AllowFullAccess') { $ConditionalAccessPolicy } else { 'AllowFullAccess' }

                    $script:ExcelSheets['External Access Controls'] = $AccessObj
                }
            } catch {
                Write-AbrSectionError -Section 'External Access Controls' -Message "$($_.Exception.Message)"
            }

            #region ACSC E8 Assessment
            if ($script:IncludeACSCe8) {
                BlankLine
                Paragraph "ACSC Essential Eight Assessment -- External Access Controls"
                $E8Defs   = Get-AbrSPE8Checks -Section 'ExternalAccess'
                $E8Vars   = @{
                    ConditionalAccessPolicy    = $ConditionalAccessPolicy
                    SignOutInactiveUsersAfter  = $SignOutInactiveUsersAfter
                    BlockDownloadPolicy        = $BlockDownloadPolicy
                    BlockDownloadLinksFileType = $BlockDownloadLinksFileType
                    DefaultSharingLinkType     = $DefaultSharingLinkType
                    RequireAcceptingAccountMatchInvitedAccount = $false
                    NotifyOwnersWhenItemsShared = $false
                }
                $E8Checks = Build-AbrSPComplianceChecks -Definitions $E8Defs -Framework 'E8' -CallerVariables $E8Vars
                New-AbrSPE8AssessmentTable -Checks $E8Checks -Name 'External Access' -TenantId $TenantId
                foreach ($row in $E8Checks) {
                    $null = $script:E8AllChecks.Add([pscustomobject](@{ Section = 'External Access' } + ($row | ConvertTo-HashTableSP)))
                }
            }
            #endregion

            #region CIS Baseline Assessment
            if ($script:IncludeCISBaseline) {
                BlankLine
                Paragraph "CIS Microsoft 365 Foundations Benchmark Assessment -- External Access Controls"
                $CISDefs   = Get-AbrSPCISChecks -Section 'ExternalAccess'
                $CISVars   = @{
                    ConditionalAccessPolicy   = $ConditionalAccessPolicy
                    SignOutInactiveUsersAfter = $SignOutInactiveUsersAfter
                    DefaultSharingLinkType    = $DefaultSharingLinkType
                }
                $CISChecks = Build-AbrSPComplianceChecks -Definitions $CISDefs -Framework 'CIS' -CallerVariables $CISVars
                New-AbrSPCISAssessmentTable -Checks $CISChecks -Name 'External Access' -TenantId $TenantId
                foreach ($row in $CISChecks) {
                    $null = $script:CISAllChecks.Add([pscustomobject](@{ Section = 'External Access' } + ($row | ConvertTo-HashTableSP)))
                }
            }
            #endregion
        }
    }

    end {
        Show-AbrDebugExecutionTime -End -TitleMessage 'External Access'
    }
}