Get-IntuneDevices.ps1

<#PSScriptInfo
.VERSION
1.0.4
 
.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.
    
.PRIVATEDATA
#>

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-RestMethod -Uri "https://graph.microsoft.com/beta/groups/$groupId/members" -Headers $headers -Method GET).value
        }
        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
    }

    $deviceResults = @()

    foreach ($member in $groupMembers) {
        $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
            }
            try {
                if ($AccessToken) {
                    Write-Host "Processing device ID: $deviceId"
                    # 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-RestMethod -Uri $uri -Headers $headers -Method GET).value
                }
                else {
                    Write-Host "Processing device ID: $deviceId"
                    $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
            try {
                if ($AccessToken) {
                    Write-Host "Processing user: $userUpn"
                    $deviceDetail = (Invoke-RestMethod -Uri "https://graph.microsoft.com/beta/deviceManagement/managedDevices?`$filter=UserPrincipalName eq '$userUpn'" -Headers $headers -Method GET).value
                }
                else {
                    Write-Host "Processing user: $userUpn"
                    $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
            exit
        }
        default {
            Write-Host "Invalid option. Try again." 
        }
    }
} while ($true)