Src/Private/Get-AbrExoMobileDevices.ps1

function Get-AbrExoMobileDevices {
    <#
    .SYNOPSIS
    Documents Exchange Online mobile device access policies (ActiveSync), device partnerships,
    and mobile device mailbox policies.
    .NOTES
        Version: 0.1.0
        Author: Pai Wei Sing
    #>

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

    begin {
        Write-PScriboMessage -Message "Collecting Exchange Online Mobile Device configuration for $TenantId."
        Show-AbrDebugExecutionTime -Start -TitleMessage 'MobileDevices'
    }

    process {
        Section -Style Heading2 'Device Access Policies' {
            Paragraph "The following section documents mobile device (ActiveSync/EAS) access policies and device partnerships for tenant $TenantId."
            BlankLine

            #region Mobile Device Access Settings (tenant-level)
            try {
                Write-Host " - Retrieving mobile device access settings..."
                # Get-MobileDeviceAccessSettings is not available in EXO v3 REST mode.
                # Use Get-ActiveSyncOrganizationSettings instead (available in all modes).
                $EasOrgSettings = Get-ActiveSyncOrganizationSettings -ErrorAction Stop

                $AccessObj = [System.Collections.ArrayList]::new()
                $accessInObj = [ordered] @{
                    'Default Access Level'              = $EasOrgSettings.DefaultAccessLevel
                    'OTA Update Enabled'                = $EasOrgSettings.OtaNotificationMailInserted
                    'User Agent Filter Enabled'         = $EasOrgSettings.UserAgentFilterEnabled
                    'Allow Non-Provisionable Devices'   = $EasOrgSettings.AllowNonProvisionableDevices
                }
                $AccessObj.Add([pscustomobject](ConvertTo-HashToYN $accessInObj)) | Out-Null

                $null = (& {
                    if ($HealthCheck.ExchangeOnline.Mailboxes) {
                        $null = ($AccessObj | Where-Object { $_.'Default Access Level' -eq 'Block' } | Set-Style -Style Warning | Out-Null)
                        $null = ($AccessObj | Where-Object { $_.'Allow Non-Provisionable Devices' -eq 'Yes' } | Set-Style -Style Warning | Out-Null)
                    }
                })

                $AccessTableParams = @{ Name = "Mobile Device Access Settings - $TenantId"; List = $true; ColumnWidths = 45, 55 }
                if ($Report.ShowTableCaptions) { $AccessTableParams['Caption'] = "- $($AccessTableParams.Name)" }
                $AccessObj | Table @AccessTableParams
            } catch {
                Write-ExoError 'MobileDevices' "Unable to retrieve mobile device access settings: $($_.Exception.Message)"
                Paragraph "Mobile device organisation settings could not be retrieved: $($_.Exception.Message)"
            }
            #endregion

            #region Mobile Device Mailbox Policies
            try {
                Write-Host " - Retrieving mobile device mailbox policies..."
                $MdmPolicies = Get-MobileDeviceMailboxPolicy -ErrorAction Stop | Sort-Object IsDefault -Descending

                if ($MdmPolicies -and @($MdmPolicies).Count -gt 0) {
                    Section -Style Heading3 'Mobile Device Mailbox Policies' {
                        Paragraph "The following $(@($MdmPolicies).Count) mobile device mailbox policy/policies are configured in tenant $TenantId."
                        BlankLine

                        $MdmObj = [System.Collections.ArrayList]::new()
                        foreach ($Policy in $MdmPolicies) {
                            $mdmInObj = [ordered] @{
                                'Policy Name'                    = $Policy.Name
                                'Default Policy'                 = $Policy.IsDefault
                                'Password Required'              = $Policy.PasswordEnabled
                                'Min Password Length'            = if ($Policy.MinPasswordLength) { $Policy.MinPasswordLength } else { 'Not Set' }
                                'Require Encryption'             = $Policy.RequireDeviceEncryption
                                'Allow Non-Provisionable Devices'= $Policy.AllowNonProvisionableDevices
                                'Allow Simple Password'          = $Policy.AllowSimplePassword
                                'Max Inactivity Lock (min)'      = if ($Policy.MaxInactivityTimeLock) { $Policy.MaxInactivityTimeLock } else { 'Not Set' }
                                'Max Password Attempts'          = if ($Policy.MaxPasswordFailedAttempts) { $Policy.MaxPasswordFailedAttempts } else { 'Not Set' }
                                'Remote Wipe Supported'          = $Policy.AllowRemoteWipe
                            }
                            $MdmObj.Add([pscustomobject](ConvertTo-HashToYN $mdmInObj)) | Out-Null
                        }

                        $null = (& {
                            if ($HealthCheck.ExchangeOnline.Mailboxes) {
                                $null = ($MdmObj | Where-Object { $_.'Password Required' -eq 'No' } | Set-Style -Style Warning | Out-Null)
                                $null = ($MdmObj | Where-Object { $_.'Require Encryption' -eq 'No' } | Set-Style -Style Warning | Out-Null)
                                $null = ($MdmObj | Where-Object { $_.'Allow Non-Provisionable Devices' -eq 'Yes' } | Set-Style -Style Warning | Out-Null)
                            }
                        })

                        $MdmTableParams = @{ Name = "Mobile Device Mailbox Policies - $TenantId"; List = $false; ColumnWidths = 14, 8, 10, 8, 10, 12, 10, 10, 10, 8 }
                        if ($Report.ShowTableCaptions) { $MdmTableParams['Caption'] = "- $($MdmTableParams.Name)" }
                        if ($MdmObj.Count -gt 0) { $MdmObj | Table @MdmTableParams }
                        $script:ExcelSheets['Mobile Device Policies'] = $MdmObj
                    }
                }
            } catch {
                Write-ExoError 'MobileDevices' "Unable to retrieve mobile device mailbox policies: $($_.Exception.Message)"
                Paragraph "Unable to retrieve mobile device mailbox policies: $($_.Exception.Message)"
            }
            #endregion

            #region Mobile Device Access Rules (block/allow by device type)
            try {
                Write-Host " - Retrieving mobile device access rules..."
                # Try Get-ActiveSyncDeviceAccessRule (available in all EXO connection modes)
                $AccessRules = Get-ActiveSyncDeviceAccessRule -ErrorAction Stop | Sort-Object Name

                if ($AccessRules -and @($AccessRules).Count -gt 0) {
                    Section -Style Heading3 'Mobile Device Access Rules' {
                        Paragraph "The following $(@($AccessRules).Count) mobile device access rule(s) control which device types can connect to Exchange Online."
                        BlankLine

                        $RuleObj = [System.Collections.ArrayList]::new()
                        foreach ($Rule in $AccessRules) {
                            $ruleInObj = [ordered] @{
                                'Rule Name'         = $Rule.Name
                                'Characteristic'    = $Rule.Characteristic
                                'Query String'      = $Rule.QueryString
                                'Access Level'      = $Rule.AccessLevel
                            }
                            $RuleObj.Add([pscustomobject]$ruleInObj) | Out-Null
                        }

                        $null = (& {
                            if ($HealthCheck.ExchangeOnline.Mailboxes) {
                                $null = ($RuleObj | Where-Object { $_.'Access Level' -eq 'Block' } | Set-Style -Style Warning | Out-Null)
                                $null = ($RuleObj | Where-Object { $_.'Access Level' -eq 'Quarantine' } | Set-Style -Style Warning | Out-Null)
                            }
                        })

                        $RuleTableParams = @{ Name = "Mobile Device Access Rules - $TenantId"; List = $false; ColumnWidths = 28, 20, 32, 20 }
                        if ($Report.ShowTableCaptions) { $RuleTableParams['Caption'] = "- $($RuleTableParams.Name)" }
                        if ($RuleObj.Count -gt 0) { $RuleObj | Table @RuleTableParams }
                        $script:ExcelSheets['Mobile Device Rules'] = $RuleObj
                    }
                } else {
                    Paragraph "No mobile device access rules are configured. The default access level from organisation settings applies to all devices."
                }
            } catch {
                Write-ExoError 'MobileDevices' "Unable to retrieve mobile device access rules: $($_.Exception.Message)"
                Paragraph "Mobile device access rules could not be retrieved. Note: device-based access controls may be managed via Microsoft Intune/Entra Conditional Access policies."
            }
            #endregion

            #region Device Partnerships (InfoLevel 2)
            if ($InfoLevel.MobileDevices -ge 2) {
                try {
                    Write-Host " - Retrieving mobile device partnerships..."
                    $Devices = Get-MobileDevice -ResultSize Unlimited -ErrorAction Stop | Sort-Object UserDisplayName, FriendlyName

                    if ($Devices -and @($Devices).Count -gt 0) {
                        Section -Style Heading3 'Mobile Device Partnerships' {
                            Paragraph "The following $(@($Devices).Count) mobile device partnership(s) are registered in tenant $TenantId."
                            BlankLine

                            $DevObj = [System.Collections.ArrayList]::new()
                            foreach ($Dev in $Devices) {
                                # Get device stats safely
                                $LastSync = '--'
                                $Status   = '--'
                                try {
                                    $Stats = Get-MobileDeviceStatistics -Mailbox $Dev.UserDisplayName -Identity $Dev.Identity -ErrorAction SilentlyContinue
                                    if ($Stats) {
                                        $LastSync = if ($Stats.LastSyncAttemptTime) { $Stats.LastSyncAttemptTime.ToString('yyyy-MM-dd') } else { 'Never' }
                                        $Status   = $Stats.Status
                                    }
                                } catch {}

                                $devInObj = [ordered] @{
                                    'User'              = $Dev.UserDisplayName
                                    'Device Name'       = $Dev.FriendlyName
                                    'Device Type'       = $Dev.DeviceType
                                    'OS'                = $Dev.DeviceOS
                                    'Model'             = $Dev.DeviceModel
                                    'Access State'      = $Dev.DeviceAccessState
                                    'Last Sync'         = $LastSync
                                    'Status'            = $Status
                                }
                                $DevObj.Add([pscustomobject]$devInObj) | Out-Null
                            }

                            $null = (& {
                                if ($HealthCheck.ExchangeOnline.Mailboxes) {
                                    $null = ($DevObj | Where-Object { $_.'Access State' -eq 'Blocked' } | Set-Style -Style Critical | Out-Null)
                                    $null = ($DevObj | Where-Object { $_.'Access State' -eq 'Quarantined' } | Set-Style -Style Warning | Out-Null)
                                    # Flag devices not synced in 90+ days
                                    $null = ($DevObj | Where-Object {
                                        $_.'Last Sync' -ne '--' -and $_.'Last Sync' -ne 'Never' -and
                                        (New-TimeSpan -Start ([datetime]::Parse($_.'Last Sync')) -End (Get-Date)).Days -gt 90
                                    } | Set-Style -Style Warning | Out-Null)
                                }
                            })

                            $DevTableParams = @{ Name = "Mobile Device Partnerships - $TenantId"; List = $false; ColumnWidths = 14, 14, 12, 12, 12, 12, 12, 12 }
                            if ($Report.ShowTableCaptions) { $DevTableParams['Caption'] = "- $($DevTableParams.Name)" }
                            if ($DevObj.Count -gt 0) { $DevObj | Table @DevTableParams }
                            $script:ExcelSheets['Mobile Devices'] = $DevObj
                        }
                    } else {
                        Paragraph "No mobile device partnerships found in tenant $TenantId."
                    }
                } catch {
                    Write-ExoError 'MobileDevices' "Unable to retrieve mobile device partnerships: $($_.Exception.Message)"
                    Paragraph "Unable to retrieve mobile device partnership data: $($_.Exception.Message)"
                }
            }
            #endregion
        }
    }

    end {
        Show-AbrDebugExecutionTime -End -TitleMessage 'MobileDevices'
    }
}