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 } ) } } } |