The Problem#
“Which group is this policy assigned to?” is a question you can answer in the portal. “Which policies are assigned to this group?” is harder. “Show me every assignment in the entire tenant” is impossible without scripting.
Intune scatters assignments across device configurations, compliance policies, Settings Catalog, endpoint security, app protection policies, app configurations, scripts, update rings, Autopilot profiles, and applications. There is no single view. When you are troubleshooting why a device is getting a specific policy, or auditing your entire assignment model, you need everything in one place.
This script walks every major Intune workload via the Graph beta API, pulls all assignments, smashes them into a consistent shape, resolves group GUIDs to names, and drops everything into Excel. One row per assignment. No clever UI, no database – just a flat table you can filter in thirty seconds.
Prerequisites#
- PowerShell 7.x with
Microsoft.Graph.AuthenticationandImportExcelmodules - Interactive sign-in via
Connect-MgGraphwith these delegated scopes:DeviceManagementApps.Read.All,DeviceManagementServiceConfig.Read.All,DeviceManagementConfiguration.Read.All,DeviceManagementManagedDevices.Read.All,DeviceManagementScripts.Read.All,Group.Read.All
- Note: This script uses the Microsoft Graph beta API. Beta endpoints can change without notice. Most Intune device management endpoints do not have v1.0 equivalents yet.
Implementation#
Graph Calls with Retry#
All Graph calls go through Invoke-MgGraphRequest (the Graph SDK handles token refresh). The wrapper retries on 429, 503, and 504 with exponential backoff:
function Invoke-GraphGetSafe {
param([Parameter(Mandatory)][string]$Uri)
for ($attempt = 1; $attempt -le 5; $attempt++) {
try {
return Invoke-MgGraphRequest -Method GET -Uri $Uri -ErrorAction Stop
}
catch {
$msg = $_.Exception.Message
if (($msg -match '429') -or ($msg -match 'Too Many Requests') -or
($msg -match '503') -or ($msg -match '504')) {
$wait = [Math]::Min(5 * [Math]::Pow(2, $attempt - 1), 60)
Start-Sleep -Seconds $wait
continue
}
return $null
}
}
return $null
}Pagination is handled by a separate function that follows @odata.nextLink until there are no more pages. Both functions are shared across all my Graph scripts – the same pattern you will see in my PIM auditing, ASR reporting, and Conditional Access export posts.
Workload Collection#
The script defines each workload as a list URI and an assignment URI template. The same Get-WorkloadAssignments function processes all of them – no copy-pasted loops:
$workloads = @(
@{
Label = 'Device Configurations'
ListUri = '.../deviceManagement/deviceConfigurations?$select=id,displayName'
Template = '.../deviceManagement/deviceConfigurations/{id}/assignments'
}
@{
Label = 'Settings Catalog / Configuration Policies'
ListUri = '.../deviceManagement/configurationPolicies?$select=id,name'
Template = '.../deviceManagement/configurationPolicies/{id}/assignments'
Type = 'configurationPolicies'
}
# ... 14 more workloads
)
foreach ($workload in $workloads) {
$rows = Get-WorkloadAssignments @params
foreach ($row in $rows) { [void]$AllIntuneAssignments.Add($row) }
}Note that configurationPolicies (Settings Catalog) uses name instead of displayName – the script handles this automatically in the normalizer.
Assignment Normalizer#
Every assignment from every workload gets flattened into the same shape. The @odata.type field on assignment targets tells you whether it targets all users, all devices, a specific group, or is an exclusion. The intent field (present on app assignments) distinguishes between required and available deployments:
switch ($Assignment.target.'@odata.type') {
'#microsoft.graph.allLicensedUsersAssignmentTarget' {
$TargetAllUsers = 'True'
if ($Assignment.intent -eq 'required') { $AssignmentIntent = 'Require' }
if ($Assignment.intent -eq 'available') { $AssignmentIntent = 'Available' }
}
'#microsoft.graph.allDevicesAssignmentTarget' { $TargetAllDevices = 'True' }
'#microsoft.graph.groupAssignmentTarget' { }
'#microsoft.graph.exclusionGroupAssignmentTarget' { $AssignmentIntent = 'Exclude' }
}Group Name Resolution#
Groups are pre-loaded once upfront and matched by GUID during processing. This avoids an API call per assignment – in a tenant with 200+ policies sharing the same 15 groups, pre-loading is the difference between finishing in two minutes and getting throttled:
$script:AllGroups = Invoke-GraphGetPaged -Uri `
'https://graph.microsoft.com/v1.0/groups?$select=id,displayName'
# In Get-Assignments:
$match = $script:AllGroups | Where-Object { $_.id -eq $groupId }
$groupName = if ($match) { $match.displayName } else { $groupId }Sample Output#
The Excel report looks like this:
| Name | Type | Assigned | AssignmentIntent | AssignedToAllUsers | AssignedToGroupName | AssignmentFilterName |
|---|---|---|---|---|---|---|
| Windows Security Baseline | intents | True | Include | False | Corp-Managed-Devices | Filter-Win11 |
| BitLocker Policy | configurationPolicies | True | Include | False | Corp-Laptops | |
| Company Portal | mobileApp | True | Require | True | ||
| Unused Compliance Policy | deviceCompliancePolicy | False | False |
The Assigned = False rows are policies that exist but are not assigned to anything – usually forgotten configurations worth cleaning up.
What to Look For#
When reviewing the report:
- Unassigned policies (
Assigned = False) – deployed but targeting nobody. Either in progress or forgotten. Clean them up or assign them. - Policies assigned to “All Users” or “All Devices” – broad-scope assignments that affect everyone. Make sure these are intentional.
- Missing exclusion groups – if your critical policies have no exclude groups, you have no break-glass path.
- Overlapping assignments – same group getting the same policy type from multiple workloads (e.g. an antivirus policy from both
intentsandconfigurationPolicies). - Assignment filters – filter columns tell you which policies use device filters and which target groups directly. Inconsistency here means inconsistent targeting.
Known Limitations#
- Beta API only. Most Intune device management endpoints do not have v1.0 equivalents. Beta endpoints can change without notice.
intentsis legacy. ThedeviceManagement/intentsendpoint returns older Endpoint Security policies (antivirus, firewall, disk encryption, security baselines). New policies of these types are created underconfigurationPoliciesand are covered separately. Both endpoints are queried so nothing is missed.windowsQualityUpdateProfilesis legacy. The script queries both the oldwindowsQualityUpdateProfilesand the newerwindowsQualityUpdatePoliciesendpoint to cover both legacy and modern quality update policies.- App protection policy types. If Microsoft adds a new app protection policy type, the script logs a warning and skips it. Check the output for warnings after running.
Security Considerations#
- Policies that show
Assigned = Falseare worth investigating – they are often forgotten configurations that were never cleaned up, or policies someone started building and abandoned. Either way, they clutter your tenant and make auditing harder. - The script reads all Intune configurations, policies, and app assignments. This is a broad view of your management posture – treat the output as sensitive.