Get-IntuneDevices.ps1
<#PSScriptInfo
.VERSION 1.0.5 .GUID e00cc407-4231-4af7-a226-f2a9b28395f3 .AUTHOR Maciej Horbacz .SYNOPSIS Retrieves Intune device details via the Microsoft Graph API. .DESCRIPTION This script leverages the Microsoft Graph API to retrieve detailed information about devices managed by Intune. It allows users to specify an Entra ID group (group can contain users and/or devices), from which it extracts device details, which can be displayed as a table or list, or exported to a CSV file. .COMPANYNAME Cloud Aligned .COPYRIGHT (c) 2025 Maciej. All rights reserved. .TAGS Intune, MicrosoftGraph, Devices, EntraID .LICENSEURI .PROJECTURI https://github.com/UniverseCitiz3n/Intune-Tools .ICONURI .EXTERNALMODULEDEPENDENCIES Microsoft.Graph .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES v1.0.0 - Initial version. v1.0.1 - Minor bug fix. v1.0.2 - Minor bug fix. v1.0.3 - Minor bug fix. v1.0.4 - Microsoft.Graph as required. v1.0.5 - Added Invoke-RecurenceRestMethod, added quicker way to resolve Intune device if group member count is over 50. .PRIVATEDATA #> #region Function Invoke-RecurenceRestMethod function Invoke-RecurenceRestMethod { param ( $Uri, $Headers, $Method = 'Get', $ContentType = "application/json" ) $irmSplat = @{ Uri = $Uri Headers = $Headers Method = $Method ContentType = $ContentType UseBasicParsing = $true } #Write-Output ('Processing URI {0}' -f $irmSplat.Uri) $QueryRequest = @() $QueryResult = @() $QueryRequest = Invoke-RestMethod @irmSplat if ($QueryRequest.value) { $QueryResult = $QueryRequest.value } else { $QueryResult = $QueryRequest } # Determine total count if available, else zero $totalCount = 0 if ($QueryRequest.'@odata.count') { $totalCount = [int]$QueryRequest.'@odata.count' } $chunkCounter = 1 if ($Uri -notlike "*`$top*") { while ($QueryRequest.PSobject.Properties.Name.Contains('@odata.nextLink') -and $QueryRequest.'@odata.nextLink') { $irmSplat.Uri = $QueryRequest.'@odata.nextLink' $chunkCounter++ if ($totalCount -gt 0) { $percent = [math]::Round(($QueryResult.Count / $totalCount) * 100) } else { $percent = $chunkCounter * 10 if ($percent -gt 100) { $percent = 100 } } Write-Progress -Activity "Fetching data" -Status "Chunk $chunkCounter (fetched $($QueryResult.Count) record(s) of $totalCount)" -PercentComplete $percent $QueryRequest = Invoke-RestMethod @irmSplat if ($QueryRequest.value) { $QueryResult += $QueryRequest.value } else { $QueryResult += $QueryRequest } } } $QueryResult } #endregion function Get-IntuneDevices { [CmdletBinding()] Param( [Parameter(Mandatory = $false)] [string]$AccessToken, [Parameter(Mandatory = $false)] [string]$TenantID ) # Ensure only one connection parameter is used if ($AccessToken -and $TenantID) { Write-Error "Provide either AccessToken or TenantID, not both." return } if ($AccessToken) { $AccessToken = $AccessToken.Trim() $headers = @{ "Authorization" = "$AccessToken" "Content-Type" = "application/json" } } elseif ($TenantID) { # No additional headers needed as the connection is already established using TenantID } else { Write-Error "No connection information provided. Run menu option 1 first." return } # Get group ID from input $groupInput = Read-Host "Enter the Entra ID group link (containing 'groupid/XXXXXXXXXXXX') or a group GUID" if ($groupInput -match "groupid/([0-9a-fA-F\-]+)") { $groupId = $matches[1] Write-Host "Detected group ID: $groupId" } elseif ($groupInput -match "^(?:\{)?[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}(?:\})?$") { $groupId = $groupInput Write-Host "Detected group GUID: $groupId" } else { Write-Error "Invalid group link or GUID." return } # Retrieve group members try { if ($AccessToken) { $groupMembers = (Invoke-RecurenceRestMethod -Uri "https://graph.microsoft.com/beta/groups/$groupId/members" -Headers $headers -Method GET) } else { # First, retrieve a temporary list to determine member types $tempMembers = Get-MgGroupMember -GroupId $groupId -All $hasUser = $false $hasDevice = $false foreach ($m in $tempMembers) { if ($m.AdditionalProperties.'@odata.type' -eq "#microsoft.graph.user") { $hasUser = $true } elseif ($m.AdditionalProperties.'@odata.type' -eq "#microsoft.graph.device") { $hasDevice = $true } } # Retrieve members with appropriate functions based on detected types $groupMembers = @() if ($hasUser) { $users = Get-MgGroupMemberAsUser -GroupId $groupId -All foreach ($user in $users) { if (-not $user.'@odata.type') { $user | Add-Member -NotePropertyName '@odata.type' -NotePropertyValue "#microsoft.graph.user" -Force } } $groupMembers += $users } if ($hasDevice) { $devices = Get-MgGroupMemberAsDevice -GroupId $groupId -All foreach ($device in $devices) { if (-not $device.'@odata.type') { $device | Add-Member -NotePropertyName '@odata.type' -NotePropertyValue "#microsoft.graph.device" -Force } } $groupMembers += $devices } } } catch { Write-Error "Error retrieving group members: $_" return } # New optimization: if the total group members count is over 50, retrieve all managed devices once and filter in memory. if ($groupMembers.Count -gt 50) { try { Write-Host "Large group detected ($($groupMembers.Count) members). Retrieving all managed devices..." if ($AccessToken) { $allDevices = (Invoke-RecurenceRestMethod -Uri "https://graph.microsoft.com/beta/deviceManagement/manageddevices" -Headers $headers -Method GET) } else { $allDevices = Get-MgDeviceManagementManagedDevice -All } } catch { Write-Warning "Failed retrieving all managed devices: $_. Continuing with individual requests." } } $deviceResults = @() # Add progress feedback for processing group members $totalMembers = $groupMembers.Count $i = 0 foreach ($member in $groupMembers) { $i++ $p = [Math]::Round(($i / $totalMembers) * 100) Write-Progress -Activity "Processing group members" -Status "Processing member $i of $totalMembers" -PercentComplete $p $memberType = $member.'@odata.type' if (-not $memberType -and $member.userPrincipalName) { $memberType = "#microsoft.graph.user" } if ($memberType -eq "#microsoft.graph.device") { # Use 'deviceId' property from device object rather than 'id' $deviceId = $member.deviceId if (-not $deviceId) { Write-Error "Device ID not found for member $($member.id). Skipping." continue } if ($allDevices) { Write-Progress -Activity "Processing devices" -Status "Processing device ID: $deviceId" $deviceDetail = $allDevices | Where-Object { $_.azureADDeviceId -like "*$deviceId*" } } else { Write-Host "Processing device ID: $deviceId" try { if ($AccessToken) { # Use the custom filter instead of device-specific filter $filter = "contains(azureADDeviceId, '$deviceId')" $uri = "https://graph.microsoft.com/beta/deviceManagement/manageddevices?`$filter=$filter" $deviceDetail = (Invoke-RecurenceRestMethod -Uri $uri -Headers $headers -Method GET) } else { $filter = "contains(azureADDeviceId, '$deviceId')" $deviceDetail = Get-MgDeviceManagementManagedDevice -Filter $filter } } catch { Write-Warning "Unable to get Intune details for device id $deviceId" continue } } foreach ($dev in $deviceDetail) { $deviceResults += [PSCustomObject]@{ DeviceName = $dev.deviceName Ownership = $dev.managedDeviceOwnerType Compliance = $dev.complianceState OS = $dev.operatingSystem OSVersion = $dev.osVersion PrimaryUserUPN = $dev.userPrincipalName LastCheckIn = $dev.lastSyncDateTime EnrollmentDate = $dev.enrolledDateTime Model = $dev.model Manufacturer = $dev.manufacturer SerialNumber = $dev.serialNumber JoinType = $dev.joinType EntraID = $dev.azureADDeviceId } } } elseif ($memberType -eq "#microsoft.graph.user") { $userUpn = $member.userPrincipalName if ($allDevices) { Write-Progress -Activity "Processing users" -Status "Processing user: $userUpn" $deviceDetail = $allDevices | Where-Object { $_.userPrincipalName -eq $userUpn } } else { Write-Host "Processing user: $userUpn" try { if ($AccessToken) { $deviceDetail = (Invoke-RecurenceRestMethod -Uri "https://graph.microsoft.com/beta/deviceManagement/manageddevices?`$filter=UserPrincipalName eq '$userUpn'" -Headers $headers -Method GET) } else { $deviceDetail = Get-MgDeviceManagementManagedDevice -Filter "UserPrincipalName eq '$userUpn'" } } catch { Write-Warning "Unable to get devices for user $userUpn" continue } } foreach ($dev in $deviceDetail) { $deviceResults += [PSCustomObject]@{ DeviceName = $dev.deviceName Ownership = $dev.managedDeviceOwnerType Compliance = $dev.complianceState OS = $dev.operatingSystem OSVersion = $dev.osVersion PrimaryUserUPN = $dev.userPrincipalName LastCheckIn = $dev.lastSyncDateTime EnrollmentDate = $dev.enrolledDateTime Model = $dev.model Manufacturer = $dev.manufacturer SerialNumber = $dev.serialNumber JoinType = $dev.joinType EntraID = $dev.azureADDeviceId } } } else { Write-Host "Skipping member with id $($member.id) and type $memberType." } } Write-Host "Retrieved $($deviceResults.Count) device record(s)." return $deviceResults } # Initialize variables $devices = $null $AccessToken = $null $Tenant = $null Clear-Host do { Write-Host "" Write-Host "Select an option:" Write-Host "1. Set Tenant or AccessToken" Write-Host "2. Disconnect from Tenant" Write-Host "3. Check group" Write-Host "4. Show devices as table" Write-Host "5. Show devices as list" Write-Host "6. Export devices to CSV" Write-Host "7. Exit" $choice = Read-Host "Enter choice (1-7)" switch ($choice) { "1" { $inputValue = Read-Host "Enter your AccessToken (if it contains a dot) or your tenant ID/domain" if ($inputValue -match "\.") { $AccessToken = $inputValue.Trim() $Tenant = $null Write-Host "AccessToken stored." } else { try { $AccessToken = $null $Tenant = $inputValue.Trim() Import-Module Microsoft.Graph.Authentication Connect-MgGraph -NoWelcome -TenantId $Tenant -Scopes "DeviceManagementManagedDevices.Read.All", "Group.Read.All", "User.Read.All", "GroupMember.Read.All" -ErrorAction Stop Write-Host "Connected to tenant $Tenant." } catch { Write-Error "Failed to connect to Microsoft Graph: $_" return } } } "2" { try { Disconnect-MgGraph Write-Host "Disconnected from tenant." $Tenant = $null $AccessToken = $null } catch { Write-Error "Failed to disconnect: $_" } } "3" { if (-not $AccessToken -and -not $Tenant) { Write-Host "No connection established. Please run option 1 first." } else { if ($AccessToken) { $devices = Get-IntuneDevices -AccessToken $AccessToken } else { $devices = Get-IntuneDevices -TenantID $Tenant } if ($devices) { Write-Host "Device details retrieved." } } } "4" { if (-not $devices -or $devices.Count -eq 0) { Write-Host "No devices loaded. Please run option 3 first." } else { $devices | Format-Table -AutoSize } } "5" { if (-not $devices -or $devices.Count -eq 0) { Write-Host "No devices loaded. Please run option 3 first." } else { $devices | Format-List * } } "6" { if (-not $devices -or $devices.Count -eq 0) { Write-Host "No devices loaded. Please run option 3 first." } else { $csvPath = Read-Host "Enter the full path for CSV export" try { $devices | Export-Csv -Path $csvPath -NoTypeInformation -Force Write-Host "Exported device details to $csvPath." } catch { Write-Error "Export failed: $_" } } } "7" { $response = Read-Host "Do you want to keep devices in current session? (y/n)" if ($response -notmatch '^(y|Y)$') { $devices = $null } else { Write-Host 'Device details are in $devices' } $AccessToken = $null break } default { Write-Host "Invalid option. Try again." } } } while ($true) |