Automating Intune Insights: Mapping Users from Device Groups with PowerShell
Digging into Additional Properties
During a recent project to upgrade groups of devices to Windows 11, I needed to identify the users associated with devices in dynamic Entra groups. I needed this to communicate to users in upgrade groups and handle automation jobs. If something fails, I want to know who the device belongs to so I can notify them or open a ticket.
The Microsoft Graph SDK for PowerShell simplifies this task with the command Get-MgDeviceRegisteredOwner, but the output objects can be difficult for non-technical users to interpret. User data is inside a sub-object called “AdditionalProperties”, containing a small set of attributes. The attributes are case-sensitive, and errors are in HTTP web codes. The Intune team needed a simple way to export the user information without dealing with AdditionalProperties.
Get-UserFromDeviceGroup
To address this, I created a PowerShell function that retrieves user information for devices in a specified Entra group and returns the results into a more user-friendly format.
What it does
Connects to Microsoft Graph (or uses your existing session)
Finds the Entra group by its display name.
Gets all devices in the group.
Loops through each device, attempting to retrieve the registered owner.
Return a custom PowerShell object with user-friendly properties.
Function Code Steps
The function is implemented as follows:
First, it makes sure the connection to Microsoft Graph is established with the necessary permissions. If not, it connects with the required scope "Directory.Read.All".
if (-not (Get-MgContext -ErrorAction SilentlyContinue).Scopes -contains "Directory.Read.All") {
Connect-MgGraph -Scopes "Directory.Read.All"
}Once connected, it retrieves the group by its display name using the Get-MgGroup with -Filter parameter. (Note that attribute name in graph are case-sensitive). If the group is not found, it throws an error.
$Group = Get-MgGroup -Filter "displayName eq '$GroupName'"
if (-not $Group) { throw "Group '$GroupName' not found." Next, it retrieves all members of the group using Get-MgGroupMember and initializes an empty array to store the results.
$Members = Get-MgGroupMember -GroupId $Group.Id -AllAt this point the function has all of the information needed to build the new hashtable. It loops through each member of the group, attempting to retrieve the registered owner of each device using Get-MgDeviceRegisteredOwner. If successful, it extracts the user information from the AdditionalProperties of the owner object.
$Owner = Get-MgDeviceRegisteredOwner -DeviceId $Member.Id -ConsistencyLevel eventual
$User = $Owner.AdditionalPropertiesFinally, the Registered Owner information is formatted into a custom PowerShell object with user-friendly properties, and added to the results array. If any errors occur during this process, they are caught and added to the results with an error message.
foreach ($Member in $Members) {
try {
$Owner = Get-MgDeviceRegisteredOwner -DeviceId $Member.Id -ConsistencyLevel eventual
$User = $Owner.AdditionalProperties
$Results += [PSCustomObject]@{
GroupName = $Group.DisplayName
DeviceName = $Member.AdditionalProperties.displayName
UserDisplayName = $User.displayName
UserPrincipalName = $User.userPrincipalName
UserEmail = $User.mail
DeviceID = $Member.Id
UserObjectId = $Owner.Id
GroupObjectId = $Group.Id
}
}
catch {
$Results += [PSCustomObject]@{
GroupName = $Group.DisplayName
DeviceName = $Member.AdditionalProperties.displayName
UserDisplayName = "Error: $($_.Exception.Message)"
UserPrincipalName = ""
UserEmail = ""
DeviceID = $Member.Id
UserObjectId = ""
GroupObjectId = $Group.Id
}
}
}
Example Usage
To use the function, call it with the -GroupName parameter with the name of the group.
$Owners = Get-UserFromDeviceGroup -GroupName "Win11_Dynamic_Upgrade_Group"
Final Output
The final output is a collection of custom PowerShell objects that contain the group name, device name, user display name, user principal name, user email, device ID, user object ID, and group object ID. This structured output is much easier for non-technical users to understand and can be exported to CSV or used in further automation tasks.
GroupName : Win11_Dynamic_Upgrade_Group
DeviceName : DLAP3412
UserDisplayName : John Smith
UserPrincipalName : jsmith@contoso.com
UserEmail : JohnSmith@contoso.com
DeviceID : xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
UserObjectId : xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
GroupObjectId : xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
The Function
The function is embedded below,and can be downloaded from this GitHub Gist. A progress bar version can be found at the bottom of this article.
| function Get-UserFromDeviceGroup { | |
| <# | |
| .SYNOPSIS | |
| Retrieves user information for devices in a specified Azure AD group using Microsoft Graph. | |
| .PARAMETER GroupName | |
| The display name of the Azure AD group containing devices. | |
| #> | |
| [CmdletBinding()] | |
| param ( | |
| [Parameter(Mandatory = $true)] | |
| [string]$GroupName | |
| ) | |
| try { | |
| if (-not (Get-MgContext -ErrorAction SilentlyContinue).Scopes -contains "Directory.Read.All") { | |
| Connect-MgGraph -Scopes "Directory.Read.All" | |
| } | |
| $Group = Get-MgGroup -Filter "displayName eq '$GroupName'" | |
| if (-not $Group) { throw "Group '$GroupName' not found." } | |
| $Members = Get-MgGroupMember -GroupId $Group.Id -All | |
| $Results = @() | |
| foreach ($Member in $Members) { | |
| try { | |
| $Owner = Get-MgDeviceRegisteredOwner -DeviceId $Member.Id -ConsistencyLevel eventual | |
| $User = $Owner.AdditionalProperties | |
| $Results += [PSCustomObject]@{ | |
| GroupName = $Group.DisplayName | |
| DeviceName = $Member.AdditionalProperties.displayName | |
| UserDisplayName = $User.displayName | |
| UserPrincipalName = $User.userPrincipalName | |
| UserEmail = $User.mail | |
| DeviceID = $Member.Id | |
| UserObjectId = $Owner.Id | |
| GroupObjectId = $Group.Id | |
| } | |
| } | |
| catch { | |
| $Results += [PSCustomObject]@{ | |
| GroupName = $Group.DisplayName | |
| DeviceName = $Member.AdditionalProperties.displayName | |
| UserDisplayName = "Error: $($_.Exception.Message)" | |
| UserPrincipalName = "" | |
| UserEmail = "" | |
| DeviceID = $Member.Id | |
| UserObjectId = "" | |
| GroupObjectId = $Group.Id | |
| } | |
| } | |
| } | |
| return $Results | |
| } | |
| catch { | |
| throw "Error: $($_.Exception.Message)" | |
| } | |
| } |
Bonus: Verbose version with Progress Bars
Because I originally wrote this for administrators to use interactively, I created a version that has Verbose logging, and progress bars for visual feedback. This one can alse be found below and at this Github Gist
| function get-userfromdevicegroup { | |
| <# | |
| .SYNOPSIS | |
| Retrieves user information for devices in a specified Azure AD group using Microsoft Graph. | |
| .DESCRIPTION | |
| This function connects to Microsoft Graph, retrieves all members (devices) of a specified Azure AD group, | |
| and for each device, uses Get-MgDeviceRegisteredOwner to find the registered user(s). | |
| It returns user and device details for reporting or automation purposes. | |
| .PARAMETER GroupName | |
| The display name of the Azure AD group containing devices. | |
| .EXAMPLE | |
| get-userfromdevicegroup -GroupName "Intune_Autopilot_Dell_Latitude_5440_W10" | |
| #> | |
| [CmdletBinding()] | |
| param ( | |
| [Parameter(Mandatory = $false, Position = 0)] | |
| [ValidateNotNullOrEmpty()] | |
| [string]$GroupName | |
| ) | |
| BEGIN { | |
| try { | |
| $ErrorActionPreference = "Stop" | |
| # Record start time for progress calculation | |
| $Count = 0 | |
| $ErrorArray = @() | |
| if ((Get-MgContext -ErrorAction SilentlyContinue).scopes -contains "Directory.Read.All") { | |
| Write-Verbose "Already connected to Microsoft Graph. Using existing context." | |
| } | |
| else { | |
| Write-Verbose "Connecting to Graph to retrieve group members..." | |
| # Connect to Azure AD | |
| Connect-MgGraph -Scopes "Directory.Read.All" | |
| } | |
| # Empty results array | |
| [PSCustomObject]$Results = @() | |
| Write-Verbose "Searching for group '$GroupName' in Azure AD..." | |
| # Retrieve the group object based on the provided group name | |
| $GroupObject = Get-MgGroup -Filter "displayname eq '$GroupName'" | |
| if (-not $GroupObject) { | |
| Throw "Group '$GroupName' not found in Azure AD." | |
| } | |
| Write-Verbose "Group $($GroupObject.DisplayName) found in Azure AD." | |
| # Retrieve members of the group | |
| $GroupMemberSplat = @{ | |
| GroupId = $GroupObject.Id | |
| All = $true | |
| } | |
| #$Members = Get-AzureADGroupMember @GroupMemberSplat | |
| $Members = Get-MgGroupMember @GroupMemberSplat | |
| Write-Verbose "Retrieved $($Members.Count) members from group '$($GroupObject.DisplayName)'." | |
| } | |
| catch { | |
| Throw "Error connecting to Azure AD or retrieving group members: $($_.Exception.Message)" | |
| } | |
| } | |
| PROCESS { | |
| Try { | |
| # Start the countdown timer | |
| $timer = [Diagnostics.Stopwatch]::StartNew() | |
| $progressSplat = @{ | |
| Activity = "Processing group members" | |
| Status = "Starting processing of group members..." | |
| PercentComplete = 0 | |
| SecondsRemaining = 60 | |
| CurrentOperation = "Initializing" | |
| Id = 0 | |
| } | |
| Write-Progress @progressSplat | |
| # Initialize FOR loop | |
| for ($i = 0; $i -lt $Members.Count; $i++) { | |
| $count++ | |
| Write-Verbose "Processing member $($i + 1) of $($Members.Count)..." | |
| $Member = $Members[$i] | |
| #Update Progress Splat with current status | |
| $progressSplat.CurrentOperation = "$($Member.DisplayName)" | |
| $progressSplat.PercentComplete = [math]::Round(($i / $Members.Count) * 100, 2) | |
| $progressSplat.SecondsRemaining = (($timer.Elapsed.TotalSeconds / $count) * ($Members.Count - $count)).ToString("F0") | |
| $progressSplat.Activity = "Processing device $($i + 1) of $($Members.Count) - $($progressSplat.PercentComplete)% complete" | |
| Write-Progress @progressSplat | |
| Try { | |
| # Get the owner of the device | |
| $OwnerSplat = @{ | |
| DeviceId = $Member.Id | |
| ConsistencyLevel = "eventual" | |
| } | |
| $RegisteredOwner = Get-MgDeviceRegisteredOwner @OwnerSplat | |
| # Collect additional properties from the object | |
| $AdditionalProperties = $RegisteredOwner.AdditionalProperties | |
| Write-Verbose "Extracted User ID: $($AdditionalProperties.userPrincipalName) from DevicePhysicalIds for computer $($Member.displayName)" | |
| Write-Verbose "Processing user data for $($AdditionalProperties.DisplayName)" | |
| # Create a custom hashtable with the required properties | |
| # and add it to the results array | |
| $Results += [PSCustomObject]@{ | |
| GroupName = $GroupObject.displayName | |
| DeviceName = $Member.additionalProperties.displayName | |
| UserFirstName = $AdditionalProperties.givenName | |
| UserLastName = $AdditionalProperties.surname | |
| UserDisplayName = $AdditionalProperties.displayName | |
| UserPrincipalName = $AdditionalProperties.userPrincipalName | |
| UserEmail = $AdditionalProperties.mail | |
| DeviceID = $Member.Id | |
| UserObjectId = $RegisteredOwner.Id | |
| GroupObjectId = $GroupObject.Id | |
| } | |
| Write-Verbose "User data for $($RegisteredOwner.DisplayName) processed successfully." | |
| } | |
| Catch { | |
| # Create a error hashtable with the error message | |
| $ErrorArray += [PSCustomObject]@{ | |
| GroupName = $GroupObject.displayName | |
| DeviceName = $Member.additionalProperties.displayName | |
| UserFirstName = $_.Exception.Message | |
| UserLastName = "N/A" | |
| UserDisplayName = "N/A" | |
| UserPrincipalName = "N/A" | |
| UserEmail = "N/A" | |
| DeviceID = $Member.Id | |
| UserObjectId = "N/A" | |
| GroupObjectId = $GroupObject.Id | |
| } | |
| continue | |
| } | |
| $count++ | |
| } | |
| } | |
| Catch { | |
| Throw "Error processing group members: $($_.Exception.Message)" | |
| } | |
| } | |
| END { | |
| If ($ErrorArray) { | |
| # If there are errors, add them to the end of results. | |
| Write-Verbose "Errors encountered during processing. Returning error results." | |
| $Results += $ErrorArray | |
| } | |
| Else { | |
| Write-Verbose "Processing completed successfully. Returning results." | |
| } | |
| Return $Results | |
| } | |
| } |



