public/Get-MsecIntuneDevice.ps1

function Get-MsecIntuneDevice {
    <#
    .SYNOPSIS
        Lists every managed device known to Intune, projected to a flat PowerShell
        shape suitable for filtering / grouping / exporting.

    .DESCRIPTION

        HOW A DEVICE WAS ENROLLED DECIDES WHETHER A USER CAN REMOVE MANAGEMENT. An Apple device
        enrolled through Automated Device Enrollment (deviceEnrollmentType appleBulkWithUser or
        appleBulkWithoutUser) has a management profile the user cannot remove. One enrolled
        manually - userEnrollment - does not, so every policy, compliance check and Conditional
        Access decision that depends on management can be ended by the person holding the laptop.
        Measured live: 9 of 19 Macs and 128 of 130 iOS devices were manually enrolled.

        IsSupervised IS NOT THE ANSWER TO THAT QUESTION. It came back True on all 19 Macs
        regardless of how they were enrolled, so filtering on it finds nothing. EnrollmentType is
        the discriminator.

        IsAutomatedEnrollment IS $null ON WINDOWS AND ANDROID, not $false. The enum reports
        windowsAzureADJoin for both Autopilot and a manual Entra join, so it cannot answer the
        question there - and a $false would claim it had. Calls Microsoft Graph /v1.0/deviceManagement/managedDevices with a $select
        for the audit-relevant columns, paginates through @odata.nextLink, and
        emits one PSCustomObject per device.

        Aggregate stats are produced in PowerShell on the consumer side - msec only
        returns the raw device list. This matches Search-MsecAzureResourceGraph /
        Get-MsecIntuneCompliancePolicy: the module returns rows, the consumer
        decides what to do with them. See the examples.

        Requires the 'DeviceManagementManagedDevices.Read.All' application
        permission. Different from DeviceManagementConfiguration.Read.All (which
        msec also has) - configuration is about POLICIES, this is about
        DEVICES. A clearer error is raised on the typical 403.

    .EXAMPLE
        # Apple devices a user could unenrol at will.
        Get-MsecIntuneDevice |
            Where-Object { $_.IsAutomatedEnrollment -eq $false } |
            Format-Table DeviceName, Os, OsVersion, EnrollmentType, ComplianceState

    .EXAMPLE
        # Compliance counts.
        Get-MsecIntuneDevice | Group-Object ComplianceState | Sort-Object Count -Descending

    .EXAMPLE
        # Devices not seen in 30 days - stale management. A device that stopped checking
        # in keeps its last compliance verdict, so these read as compliant while being
        # entirely unverified.
        Get-MsecIntuneDevice |
            Where-Object { $_.LastSyncDateTime -lt (Get-Date).AddDays(-30) }

    .EXAMPLE
        # OS family breakdown.
        Get-MsecIntuneDevice | Group-Object Os | Select-Object Name, Count

    .EXAMPLE
        # Snapshot-style headline percentages for an archive or a posture report.
        $d = Get-MsecIntuneDevice
        [pscustomobject]@{
            Total = $d.Count
            Compliant = ($d | Where-Object ComplianceState -eq 'compliant').Count
            Noncompliant = ($d | Where-Object ComplianceState -eq 'noncompliant').Count
            InGracePeriod = ($d | Where-Object ComplianceState -eq 'inGracePeriod').Count
            CompliantPercent = if ($d.Count) {
                [math]::Round(($d | Where-Object ComplianceState -eq 'compliant').Count / $d.Count * 100, 2)
            } else { 0 }
        }

    .OUTPUTS
        PSCustomObject per device, with the columns documented in the .NOTES.

    .NOTES
        Projected columns (Graph field -> output property):
          id -> Id
          deviceName -> DeviceName
          userPrincipalName -> UserPrincipalName
          userDisplayName -> UserDisplayName
          operatingSystem -> Os
          osVersion -> OsVersion
          model -> Model
          manufacturer -> Manufacturer
          complianceState -> ComplianceState
          complianceGracePeriodExpirationDateTime -> ComplianceGraceUntil (null when no grace)
          managementState -> ManagementState
          managementAgent -> ManagementAgent
          managedDeviceOwnerType -> Ownership ('company' / 'personal' / 'unknown')
          isEncrypted -> IsEncrypted
          jailBroken -> Jailbroken
          azureADRegistered -> EntraRegistered
          enrolledDateTime -> EnrolledDateTime
          lastSyncDateTime -> LastSyncDateTime
          serialNumber -> SerialNumber
    #>

    [CmdletBinding()]
    param()

    Assert-MsecSession

    # $select trims the response to ~20% of the full managedDevice shape. Stops
    # us paging through 80+ noisy columns we don't need. Comma-joined so we don't
    # have to URL-encode anything ourselves; Graph accepts the bare list.
    $select = @(
        'id'
        'deviceName'
        'userPrincipalName'
        'userDisplayName'
        'operatingSystem'
        'osVersion'
        'model'
        'manufacturer'
        'complianceState'
        'complianceGracePeriodExpirationDateTime'
        'managementState'
        'managementAgent'
        'managedDeviceOwnerType'
        'isEncrypted'
        'jailBroken'
        'azureADRegistered'
        'enrolledDateTime'
        'lastSyncDateTime'
        'serialNumber'
        # How the device was enrolled decides whether a user can simply remove management.
        # NB autopilotEnrolled is beta-only and 400s on v1.0 - do not add it here.
        'deviceEnrollmentType'
        'isSupervised'
        'enrollmentProfileName'
    ) -join ','

    $path = "/v1.0/deviceManagement/managedDevices?`$select=$select"

    try {
        $devices = @(Invoke-MsecGraphRequest -Path $path -All)
    }
    catch {
        if ($_.Exception.Message -match '403|Forbidden') {
            throw "Forbidden when calling /deviceManagement/managedDevices. The msec app needs the 'DeviceManagementManagedDevices.Read.All' application permission (admin consent required). Re-run New-MsecApp to add and consent it. Original error: $($_.Exception.Message)"
        }
        throw
    }

    foreach ($d in $devices) {
        # Graph returns a sentinel '9999-12-31T...' when no grace period is set;
        # surface that as $null so the caller can filter / compare cleanly.
        $grace = $null
        if ($d.complianceGracePeriodExpirationDateTime -and
            $d.complianceGracePeriodExpirationDateTime -notmatch '^9999-') {
            $grace = [datetime]$d.complianceGracePeriodExpirationDateTime
        }

        [PSCustomObject]@{
            Id                   = $d.id
            DeviceName           = $d.deviceName
            UserPrincipalName    = $d.userPrincipalName
            UserDisplayName      = $d.userDisplayName
            Os                   = $d.operatingSystem
            OsVersion            = $d.osVersion
            Model                = $d.model
            Manufacturer         = $d.manufacturer
            ComplianceState      = $d.complianceState
            ManagementState      = $d.managementState
            ManagementAgent      = $d.managementAgent
            Ownership            = $d.managedDeviceOwnerType
            IsEncrypted          = $d.isEncrypted
            Jailbroken           = $d.jailBroken
            EntraRegistered      = $d.azureADRegistered
            EnrolledDateTime     = if ($d.enrolledDateTime) { [datetime]$d.enrolledDateTime } else { $null }
            LastSyncDateTime     = if ($d.lastSyncDateTime) { [datetime]$d.lastSyncDateTime } else { $null }
            ComplianceGraceUntil = $grace
            SerialNumber         = $d.serialNumber
            EnrollmentType       = $d.deviceEnrollmentType
            IsSupervised         = $d.isSupervised
            EnrollmentProfile    = $d.enrollmentProfileName
            # $true only where the enum actually proves automated enrolment, $null where it
            # cannot - see the help. Never $false on a platform the enum cannot answer for.
            IsAutomatedEnrollment = $(
                switch -Wildcard ([string] $d.deviceEnrollmentType) {
                    'appleBulk*'      { $true;  break }   # ADE / Apple Business Manager
                    'appleUser*'      { $false; break }   # Apple User Enrollment - removable
                    'userEnrollment'  { $false; break }   # manual - removable
                    ''                { $null;  break }
                    default           { $null }           # Windows/Android: enum does not say
                }
            )
        }
    }
}