Overview#
Enumerates device configuration profiles (v1.0), settings catalog policies (beta), compliance policies (v1.0), administrative templates (beta) and, with -IncludeApps, assigned apps (v1.0), reads the /assignments collection of each one and emits one row per assignment with the resolved target (All users, All devices, included or excluded group) and assignment filter. Group display names are resolved once and cached; deleted groups are shown as <deleted group>. Policies without any assignment are listed as warnings at the end.
Safety: Report only — makes no changes to your tenant. Run Get-Help .\Get-IntunePolicyAssignments.ps1 -Full for the complete help text.
Parameters#
| Parameter | What it does |
|---|---|
-GroupName | Wildcard pattern (for example 'SG-Intune-*'). Only assignments that target a matching group are returned. |
-IncludeApps | Also include app assignments (mobileApps with isAssigned eq true). Requests DeviceManagementApps.Read.All. |
-OutputPath | CSV file to create. Defaults to .\Reports\IntunePolicyAssignments_yyyyMMdd-HHmm.csv; the folder is created when missing. |
-PassThru | Also emit the assignment rows to the pipeline. |
Examples#
PS> .\Get-IntunePolicyAssignments.ps1Exports every policy assignment and warns about policies that are not assigned to anything.
PS> .\Get-IntunePolicyAssignments.ps1 -GroupName 'SG-Pilot*' -IncludeApps -PassThru | Format-Table PolicyType, PolicyName, Intent, GroupName, FilterTypeShows which policies and apps are targeted at the pilot groups, including assignment filters.
Permissions, modules and notes#
Author : Omer Eltayeb Blog : https://www.oeltayeb.com GitHub : https://github.com/omer-eltayeb Version : 1.0.0 Requires : PowerShell 5.1 or 7.x, Microsoft.Graph.Authentication Permissions : DeviceManagementConfiguration.Read.All, Group.Read.All and, with -IncludeApps, DeviceManagementApps.Read.All (delegated) plus an Intune RBAC role such as Read Only Operator Notes : Settings catalog policies and administrative templates are only exposed on the beta endpoint. One Graph call is made per policy (plus one per distinct group); a 200 ms pause between policies keeps the run inside the Intune throttling limits. FilterId refers to an assignment filter under Devices > Assignment filters.
Full source#
<#
.SYNOPSIS
Builds a "who gets what" assignment matrix for Intune policies and (optionally) apps.
.DESCRIPTION
Enumerates device configuration profiles (v1.0), settings catalog policies (beta), compliance
policies (v1.0), administrative templates (beta) and, with -IncludeApps, assigned apps (v1.0),
reads the /assignments collection of each one and emits one row per assignment with the
resolved target (All users, All devices, included or excluded group) and assignment filter.
Group display names are resolved once and cached; deleted groups are shown as <deleted group>.
Policies without any assignment are listed as warnings at the end.
.PARAMETER GroupName
Wildcard pattern (for example 'SG-Intune-*'). Only assignments that target a matching group are returned.
.PARAMETER IncludeApps
Also include app assignments (mobileApps with isAssigned eq true). Requests DeviceManagementApps.Read.All.
.PARAMETER OutputPath
CSV file to create. Defaults to .\Reports\IntunePolicyAssignments_yyyyMMdd-HHmm.csv; the folder is created when missing.
.PARAMETER PassThru
Also emit the assignment rows to the pipeline.
.EXAMPLE
PS> .\Get-IntunePolicyAssignments.ps1
Exports every policy assignment and warns about policies that are not assigned to anything.
.EXAMPLE
PS> .\Get-IntunePolicyAssignments.ps1 -GroupName 'SG-Pilot*' -IncludeApps -PassThru | Format-Table PolicyType, PolicyName, Intent, GroupName, FilterType
Shows which policies and apps are targeted at the pilot groups, including assignment filters.
.NOTES
Author : Omer Eltayeb
Blog : https://www.oeltayeb.com
GitHub : https://github.com/omer-eltayeb
Version : 1.0.0
Requires : PowerShell 5.1 or 7.x, Microsoft.Graph.Authentication
Permissions : DeviceManagementConfiguration.Read.All, Group.Read.All and, with -IncludeApps,
DeviceManagementApps.Read.All (delegated) plus an Intune RBAC role such as Read Only Operator
Notes : Settings catalog policies and administrative templates are only exposed on the beta endpoint.
One Graph call is made per policy (plus one per distinct group); a 200 ms pause between policies
keeps the run inside the Intune throttling limits. FilterId refers to an assignment filter under
Devices > Assignment filters.
.LINK
https://learn.microsoft.com/graph/api/intune-deviceconfig-deviceconfigurationassignment-list
.LINK
https://learn.microsoft.com/graph/api/intune-apps-mobileappassignment-list
#>
#Requires -Version 5.1
#Requires -Modules Microsoft.Graph.Authentication
[CmdletBinding()]
param(
[Parameter()]
[string]$GroupName,
[Parameter()]
[switch]$IncludeApps,
[Parameter()]
[string]$OutputPath,
[Parameter()]
[switch]$PassThru
)
$ErrorActionPreference = 'Stop'
#region Helpers
function Connect-GraphIfNeeded {
<# Connects to Microsoft Graph only when there is no usable session for the required scopes. #>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string[]]$Scopes
)
$context = Get-MgContext
$missingScopes = @()
if ($null -ne $context) {
$missingScopes = @($Scopes | Where-Object { $context.Scopes -notcontains $_ })
}
if ($null -eq $context -or $missingScopes.Count -gt 0) {
Write-Verbose "Connecting to Microsoft Graph with scopes: $($Scopes -join ', ')"
Connect-MgGraph -Scopes $Scopes -NoWelcome -ErrorAction Stop | Out-Null
}
else {
Write-Verbose "Reusing existing Microsoft Graph session for $($context.Account)."
}
}
function Invoke-GraphPaged {
<# GET helper that follows @odata.nextLink and returns every item in 'value'. #>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Uri,
[Parameter()]
[hashtable]$Headers
)
$results = New-Object -TypeName System.Collections.Generic.List[object]
$nextLink = $Uri
while (-not [string]::IsNullOrEmpty($nextLink)) {
$requestParams = @{ Method = 'GET'; Uri = $nextLink; OutputType = 'PSObject'; ErrorAction = 'Stop' }
if ($null -ne $Headers) { $requestParams['Headers'] = $Headers }
$response = Invoke-MgGraphRequest @requestParams
if ($null -ne $response.PSObject.Properties['value']) {
foreach ($item in $response.value) { $results.Add($item) }
}
elseif ($null -ne $response) {
$results.Add($response)
}
$nextLink = $response.'@odata.nextLink'
}
return $results
}
function Get-GroupDisplayName {
<# Resolves a group id to its display name through a script-level cache; deleted groups return '<deleted group>'. #>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$GroupId
)
if ($script:groupNameCache.ContainsKey($GroupId)) {
return $script:groupNameCache[$GroupId]
}
$name = '<unresolved>'
try {
$group = Invoke-MgGraphRequest -Method GET -Uri ('https://graph.microsoft.com/v1.0/groups/{0}?$select=displayName' -f $GroupId) -OutputType PSObject -ErrorAction Stop
$name = [string]$group.displayName
}
catch {
$errorText = '{0} {1}' -f $_.Exception.Message, $_.ErrorDetails.Message
if ($errorText -match 'NotFound|\b404\b|does not exist') {
$name = '<deleted group>'
}
else {
Write-Warning ('Could not resolve group {0}: {1}' -f $GroupId, $_.Exception.Message)
}
}
$script:groupNameCache[$GroupId] = $name
return $name
}
#endregion Helpers
#region Main
if ([string]::IsNullOrWhiteSpace($OutputPath)) {
$reportFolder = Join-Path -Path (Get-Location).Path -ChildPath 'Reports'
$OutputPath = Join-Path -Path $reportFolder -ChildPath ('IntunePolicyAssignments_{0}.csv' -f (Get-Date -Format 'yyyyMMdd-HHmm'))
}
$outputFolder = Split-Path -Path $OutputPath -Parent
if (-not [string]::IsNullOrWhiteSpace($outputFolder) -and -not (Test-Path -Path $outputFolder)) {
New-Item -Path $outputFolder -ItemType Directory -Force | Out-Null
}
$scopes = @('DeviceManagementConfiguration.Read.All', 'Group.Read.All')
if ($IncludeApps) { $scopes += 'DeviceManagementApps.Read.All' }
try {
Connect-GraphIfNeeded -Scopes $scopes
}
catch {
throw "Failed to connect to Microsoft Graph: $($_.Exception.Message)"
}
$graphV1 = 'https://graph.microsoft.com/v1.0'
$graphBeta = 'https://graph.microsoft.com/beta' # beta: settings catalog policies and administrative templates are not exposed in v1.0
$sources = @(
[PSCustomObject]@{ PolicyType = 'DeviceConfiguration'; BaseUri = "$graphV1/deviceManagement/deviceConfigurations"; ListQuery = '?$select=id,displayName'; NameProperty = 'displayName' }
[PSCustomObject]@{ PolicyType = 'SettingsCatalog'; BaseUri = "$graphBeta/deviceManagement/configurationPolicies"; ListQuery = '?$select=id,name'; NameProperty = 'name' }
[PSCustomObject]@{ PolicyType = 'Compliance'; BaseUri = "$graphV1/deviceManagement/deviceCompliancePolicies"; ListQuery = '?$select=id,displayName'; NameProperty = 'displayName' }
[PSCustomObject]@{ PolicyType = 'AdministrativeTemplate'; BaseUri = "$graphBeta/deviceManagement/groupPolicyConfigurations"; ListQuery = '?$select=id,displayName'; NameProperty = 'displayName' }
)
if ($IncludeApps) {
$sources += [PSCustomObject]@{ PolicyType = 'App'; BaseUri = "$graphV1/deviceAppManagement/mobileApps"; ListQuery = '?$filter=isAssigned eq true&$select=id,displayName'; NameProperty = 'displayName' }
}
$targetTypeMap = @{
'#microsoft.graph.allLicensedUsersAssignmentTarget' = 'AllUsers'
'#microsoft.graph.allDevicesAssignmentTarget' = 'AllDevices'
'#microsoft.graph.groupAssignmentTarget' = 'Group'
'#microsoft.graph.exclusionGroupAssignmentTarget' = 'ExclusionGroup'
}
$script:groupNameCache = @{}
$rows = New-Object -TypeName System.Collections.Generic.List[object]
$unassigned = New-Object -TypeName System.Collections.Generic.List[object]
$policyCount = 0
foreach ($source in $sources) {
Write-Verbose ('Listing {0} from {1}' -f $source.PolicyType, $source.BaseUri)
try {
$policies = @(Invoke-GraphPaged -Uri ($source.BaseUri + $source.ListQuery))
}
catch {
Write-Warning ('Could not list {0} policies: {1}' -f $source.PolicyType, $_.Exception.Message)
continue
}
$nameProperty = $source.NameProperty
$index = 0
foreach ($policy in $policies) {
$index++
$policyCount++
$policyName = [string]$policy.$nameProperty
Write-Progress -Activity ('Reading {0} assignments' -f $source.PolicyType) -Status ('{0} of {1}: {2}' -f $index, $policies.Count, $policyName) -PercentComplete ([int](($index / $policies.Count) * 100))
try {
$assignments = @(Invoke-GraphPaged -Uri ('{0}/{1}/assignments' -f $source.BaseUri, $policy.id))
}
catch {
Write-Warning ("Could not read assignments of {0} '{1}': {2}" -f $source.PolicyType, $policyName, $_.Exception.Message)
continue
}
if ($assignments.Count -eq 0) {
$unassigned.Add(('{0}: {1}' -f $source.PolicyType, $policyName))
}
foreach ($assignment in $assignments) {
$target = $assignment.target
$odataType = [string]$target.'@odata.type'
$targetType = $targetTypeMap[$odataType]
if ([string]::IsNullOrEmpty($targetType)) { $targetType = $odataType }
$groupId = $null
$resolvedGroupName = $null
if ($targetType -eq 'Group' -or $targetType -eq 'ExclusionGroup') {
$groupId = [string]$target.groupId
$resolvedGroupName = Get-GroupDisplayName -GroupId $groupId
}
# Apps carry a real intent (required/available/uninstall); configuration policies only include or exclude.
if ($source.PolicyType -eq 'App') { $intent = [string]$assignment.intent }
elseif ($targetType -eq 'ExclusionGroup') { $intent = 'Exclude' }
else { $intent = 'Include' }
$filterType = [string]$target.deviceAndAppManagementAssignmentFilterType
if ([string]::IsNullOrEmpty($filterType)) { $filterType = 'none' }
$rows.Add([PSCustomObject]@{
PolicyType = $source.PolicyType
PolicyName = $policyName
PolicyId = $policy.id
Intent = $intent
TargetType = $targetType
GroupId = $groupId
GroupName = $resolvedGroupName
FilterId = $target.deviceAndAppManagementAssignmentFilterId
FilterType = $filterType
})
}
Start-Sleep -Milliseconds 200
}
Write-Progress -Activity ('Reading {0} assignments' -f $source.PolicyType) -Completed
}
$output = $rows
if (-not [string]::IsNullOrWhiteSpace($GroupName)) {
$output = @($rows | Where-Object { $_.GroupName -like $GroupName })
Write-Verbose ('{0} of {1} assignments target groups matching "{2}".' -f $output.Count, $rows.Count, $GroupName)
}
if ($output.Count -gt 0) {
$output | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8
Write-Host ('Report written to {0}' -f $OutputPath) -ForegroundColor Green
}
else {
Write-Warning 'No assignments matched the specified criteria; no CSV file was written.'
}
Write-Host ''
Write-Host ('Policies scanned : {0}' -f $policyCount) -ForegroundColor Cyan
Write-Host ('Assignments reported : {0}' -f $output.Count) -ForegroundColor Cyan
foreach ($group in ($output | Group-Object -Property PolicyType | Sort-Object -Property Name)) {
Write-Host (' {0,-24} {1,6}' -f $group.Name, $group.Count)
}
if ($unassigned.Count -gt 0) {
Write-Warning ('{0} policies have no assignments at all:' -f $unassigned.Count)
foreach ($item in $unassigned) {
Write-Warning (' {0}' -f $item)
}
}
if ($PassThru) {
$output
}
#endregion Main