Public/Get-IntuneDeviceAssignment.ps1
|
function Get-IntuneDeviceAssignment { [CmdletBinding()] param( [Parameter(Mandatory = $false)] [string]$DeviceNames, [Parameter(Mandatory = $false)] [switch]$ExportToCSV, [Parameter(Mandatory = $false)] [string]$ExportPath, [Parameter(Mandatory = $false)] [string]$ScopeTagFilter ) Write-Host "Device selection chosen" -ForegroundColor Green # Get Device names from parameter or prompt if ($DeviceNames) { $deviceInput = $DeviceNames } else { # Prompt for one or more Device Names Write-Host "Please enter Device Name(s), separated by commas (,): " -ForegroundColor Cyan $deviceInput = Read-Host } if ([string]::IsNullOrWhiteSpace($deviceInput)) { Write-Host "No device name provided. Please try again." -ForegroundColor Red return } # Distinct name from the [string]$DeviceNames parameter: assigning this array to the # case-insensitively identical, type-constrained parameter variable would coerce it # back into one space-joined string and break multi-device input. $deviceNameList = $deviceInput -split ',' | ForEach-Object { $_.Trim() } $exportData = [System.Collections.ArrayList]::new() $categories = Get-IntuneCategoryDefinition -Audience 'DeviceContext' # Categories the legacy code fetched only for Windows (or unknown-OS) devices $windowsOnlyCategoryIds = @('DeploymentProfiles', 'ESPProfiles', 'CloudPCProvisioningPolicies', 'CloudPCUserSettings') # Shared across devices so each entity set is fetched from Graph once per run $entityCache = @{} # Legacy device table: four columns (no Platform), "No Assignment" fallback, # red rows for exclusions. Kept local because it differs from Show-CategoryResultTable. function Format-PolicyTable { param ( [string]$Title, [object[]]$Policies, [scriptblock]$GetName ) $tableSeparator = Get-Separator # Create prominent section header $headerSeparator = "-" * ($Title.Length + 16) Write-Host "`n$headerSeparator" -ForegroundColor Cyan Write-Host "------- $Title -------" -ForegroundColor Cyan Write-Host "$headerSeparator" -ForegroundColor Cyan if ($Policies.Count -eq 0) { Write-Host "No $Title found for this device." -ForegroundColor Gray Write-Host $tableSeparator -ForegroundColor Gray Write-Host "" return } # Create table header $headerFormat = "{0,-45} {1,-20} {2,-35} {3,-30}" -f "Policy Name", "Scope Tags", "ID", "Assignment" Write-Host $headerFormat -ForegroundColor Yellow Write-Host $tableSeparator -ForegroundColor Gray # Display each policy in table format foreach ($policy in $Policies) { $name = & $GetName $policy if ($name.Length -gt 42) { $name = $name.Substring(0, 39) + "..." } $scopeTags = Get-ScopeTagNames -ScopeTagIds $policy.roleScopeTagIds -ScopeTagLookup $script:ScopeTagLookup if ($scopeTags.Length -gt 17) { $scopeTags = $scopeTags.Substring(0, 14) + "..." } $id = $policy.id if ($id.Length -gt 32) { $id = $id.Substring(0, 29) + "..." } $assignment = if ($policy.AssignmentReason) { $policy.AssignmentReason } else { "No Assignment" } if ($assignment.Length -gt 27) { $assignment = $assignment.Substring(0, 24) + "..." } $rowFormat = "{0,-45} {1,-20} {2,-35} {3,-30}" -f $name, $scopeTags, $id, $assignment if ($assignment -like "Excluded*" -or $assignment -like "*Exclusion*") { Write-Host $rowFormat -ForegroundColor Red } else { Write-Host $rowFormat -ForegroundColor White } } Write-Host $tableSeparator -ForegroundColor Gray } foreach ($deviceName in $deviceNameList) { Write-Host "`nProcessing device: $deviceName" -ForegroundColor Yellow # Check if input is a GUID (Object ID) $deviceInfo = $null if ($deviceName -match '^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$') { try { $selectProps = "id,displayName,operatingSystem,operatingSystemVersion,managementType,deviceOwnership,trustType,isCompliant,isManaged,approximateLastSignInDateTime,manufacturer,model,enrollmentProfileName" $directDevice = Invoke-MgGraphRequest -Uri "$script:GraphEndpoint/beta/devices/$($deviceName)?`$select=$selectProps" -Method Get $deviceInfo = @{ Id = $directDevice.id DisplayName = $directDevice.displayName OperatingSystem = $directDevice.operatingSystem Success = $true MultipleFound = $false AllDevices = $null } } catch { Write-Host "No device found with Object ID: $deviceName" -ForegroundColor Red continue } } else { # Get Device Info by display name $deviceInfo = Get-DeviceInfo -DeviceName $deviceName } # Handle multiple devices found if ($deviceInfo.MultipleFound) { if ($DeviceNames) { Write-Host "Multiple devices found with name '$deviceName'. Please use the Object ID instead:" -ForegroundColor Red foreach ($d in $deviceInfo.AllDevices) { $lastSignIn = if ($d.approximateLastSignInDateTime) { ([datetime]$d.approximateLastSignInDateTime).ToString("yyyy-MM-dd") } else { "N/A" } Write-Host " - $($d.displayName) | OS: $($d.operatingSystem) $($d.operatingSystemVersion) | Trust: $($d.trustType) | Ownership: $($d.deviceOwnership) | Last sign-in: $lastSignIn | ID: $($d.id)" -ForegroundColor Yellow } continue } Write-Host "`nMultiple devices found with name '$deviceName':" -ForegroundColor Yellow Write-Host "" for ($i = 0; $i -lt $deviceInfo.AllDevices.Count; $i++) { $d = $deviceInfo.AllDevices[$i] $lastSignIn = if ($d.approximateLastSignInDateTime) { ([datetime]$d.approximateLastSignInDateTime).ToString("yyyy-MM-dd") } else { "N/A" } $managedStatus = if ($d.isManaged) { "Managed" } else { "Not managed" } $compliantStatus = if ($d.isCompliant) { "Compliant" } else { "Not compliant" } Write-Host " [$($i + 1)] $($d.displayName)" -ForegroundColor Cyan Write-Host " OS: $($d.operatingSystem) $($d.operatingSystemVersion) | Trust: $($d.trustType) | Ownership: $($d.deviceOwnership)" -ForegroundColor Gray Write-Host " $managedStatus | $compliantStatus | Last sign-in: $lastSignIn" -ForegroundColor Gray Write-Host " Object ID: $($d.id)" -ForegroundColor Gray } Write-Host " [0] Skip this device" -ForegroundColor Gray Write-Host "" Write-Host "Select a device (1-$($deviceInfo.AllDevices.Count)) or 0 to skip: " -ForegroundColor Cyan -NoNewline $selection = Read-Host if ($selection -match '^\d+$' -and [int]$selection -ge 1 -and [int]$selection -le $deviceInfo.AllDevices.Count) { $selectedDevice = $deviceInfo.AllDevices[[int]$selection - 1] $deviceInfo = @{ Id = $selectedDevice.id DisplayName = $selectedDevice.displayName OperatingSystem = $selectedDevice.operatingSystem Success = $true MultipleFound = $false AllDevices = $null } } else { Write-Host "Skipping device: $deviceName" -ForegroundColor Yellow continue } } if (-not $deviceInfo.Success) { Write-Host "Device not found: $deviceName" -ForegroundColor Red Write-Host "Please verify the device name is correct." -ForegroundColor Yellow continue } $deviceOS = $deviceInfo.OperatingSystem if ($deviceOS) { Write-Host "Device OS: $deviceOS" -ForegroundColor Green } # Get Device Group Memberships try { $groupMemberships = Get-GroupMemberships -ObjectId $deviceInfo.Id -ObjectType "Device" Write-Host "Device Group Memberships: $($groupMemberships.displayName -join ', ')" -ForegroundColor Green } catch { Write-Host "Error fetching group memberships for device: $deviceName" -ForegroundColor Red Write-Host "Error details: $($_.Exception.Message)" -ForegroundColor Red continue } Write-Host "Fetching Intune Profiles and Applications for the device..." -ForegroundColor Yellow # Autopilot/ESP/Windows 365 stay bucket-only (export parity, never fetched) unless # the device is Windows or its OS is unknown, matching the legacy conditional fetch. $isWindowsDevice = (-not $deviceOS) -or ($deviceOS -eq "Windows") foreach ($category in $categories) { if ($category.Id -in $windowsOnlyCategoryIds) { $category.BucketOnly = -not $isWindowsDevice } } # The legacy code platform-filtered apps and App Protection policies before fetching # their assignments; every other category is platform-checked after reason resolution. $entityPreFilter = { param($entity, $category) switch ($category.Kind) { 'MobileApps' { Test-AppPlatformCompatibility -DeviceOS $deviceOS -App $entity } 'AppProtection' { Test-PlatformCompatibility -DeviceOS $deviceOS -Policy $entity } default { $true } } } $processEntity = { param($ctx) $entity = $ctx.Entity $bucketKey = $ctx.Category.BucketKeys[0] if ($ctx.Category.Kind -eq 'MobileApps') { # Winning-assignment walk: an exclusion for a member group wins immediately; # otherwise the intent comes from the FIRST matching inclusion assignment # while the reason text reflects the last one (historical behavior, F14). $isExcluded = $false $isIncluded = $false $inclusionReason = "" $exclusionReason = "" $inclusionAssignment = $null $exclusionAssignment = $null foreach ($assignment in $ctx.RawAssignments) { if ($assignment.target.'@odata.type' -eq '#microsoft.graph.exclusionGroupAssignmentTarget' -and $groupMemberships.id -contains $assignment.target.groupId) { $isExcluded = $true $groupInfo = Get-GroupInfo -GroupId $assignment.target.groupId $exclusionReason = "Excluded via group: $($groupInfo.DisplayName)" $exclusionAssignment = $assignment break } elseif ($assignment.target.'@odata.type' -eq '#microsoft.graph.allDevicesAssignmentTarget') { if (-not $isIncluded) { $inclusionAssignment = $assignment } $isIncluded = $true $inclusionReason = "All Devices" } elseif ($assignment.target.'@odata.type' -eq '#microsoft.graph.groupAssignmentTarget' -and $groupMemberships.id -contains $assignment.target.groupId) { if (-not $isIncluded) { $inclusionAssignment = $assignment } $isIncluded = $true $groupInfo = Get-GroupInfo -GroupId $assignment.target.groupId $inclusionReason = "Group Assignment - $($groupInfo.DisplayName)" } } $winningAssignment = if ($isExcluded) { $exclusionAssignment } elseif ($isIncluded) { $inclusionAssignment } else { $null } if ($winningAssignment) { $reasonText = if ($isExcluded) { $exclusionReason } else { $inclusionReason } $suffix = Format-AssignmentFilter ` -FilterId $winningAssignment.target.deviceAndAppManagementAssignmentFilterId ` -FilterType $winningAssignment.target.deviceAndAppManagementAssignmentFilterType $appWithReason = $entity.PSObject.Copy() $appWithReason | Add-Member -NotePropertyName 'AssignmentReason' -NotePropertyValue "$reasonText$suffix" -Force # F14: the bucket comes from the winning assignment's intent switch ($winningAssignment.intent) { "required" { $ctx.Buckets['AppsRequired'].Add($appWithReason) } "available" { $ctx.Buckets['AppsAvailable'].Add($appWithReason) } "uninstall" { $ctx.Buckets['AppsUninstall'].Add($appWithReason) } } } return } if ($ctx.Category.Kind -eq 'AppProtection') { # Membership check: All Devices always counts, group targets only when the # device is a member, All Users targets are dropped (device context). $relevantAssignments = @($ctx.Assignments | Where-Object { $_.Reason -eq 'All Devices' -or ($_.Reason -in @('Group Assignment', 'Group Exclusion') -and $groupMemberships.id -contains $_.GroupId) }) if ($relevantAssignments.Count -gt 0) { $assignmentSummary = $relevantAssignments | ForEach-Object { Format-AssignmentSummaryLine -Assignment $_ } $entity | Add-Member -NotePropertyName 'AssignmentSummary' -NotePropertyValue ($assignmentSummary -join "; ") -Force $ctx.Buckets[$bucketKey].Add($entity) } return } if ($ctx.Category.Id -in $windowsOnlyCategoryIds) { # First-match walk with no platform filtering: All Devices or a member group # assignment includes; a member group exclusion shows as "Excluded". foreach ($assignment in $ctx.Assignments) { if (($assignment.Reason -eq "All Devices") -or ($assignment.Reason -eq "Group Assignment" -and $groupMemberships.id -contains $assignment.GroupId)) { $suffix = Format-AssignmentFilter -FilterId $assignment.FilterId -FilterType $assignment.FilterType $entity | Add-Member -NotePropertyName 'AssignmentReason' -NotePropertyValue "$($assignment.Reason)$suffix" -Force $ctx.Buckets[$bucketKey].Add($entity) return } elseif ($assignment.Reason -eq "Group Exclusion" -and $groupMemberships.id -contains $assignment.GroupId) { $suffix = Format-AssignmentFilter -FilterId $assignment.FilterId -FilterType $assignment.FilterType $entity | Add-Member -NotePropertyName 'AssignmentReason' -NotePropertyValue "Excluded$suffix" -Force $ctx.Buckets[$bucketKey].Add($entity) return } } return } # Standard categories and both Endpoint Security phases: resolve the reason # (All Devices counts as inclusion) first, then drop other-platform policies. $reason = Resolve-AssignmentReason -Assignments $ctx.Assignments -GroupMembershipIds $groupMemberships.id -IncludeReasons @("All Devices") if ($reason -and (Test-PlatformCompatibility -DeviceOS $deviceOS -Policy $entity)) { $entity | Add-Member -NotePropertyName 'AssignmentReason' -NotePropertyValue $reason -Force $ctx.Buckets[$bucketKey].Add($entity) } } $scanResult = Invoke-IntuneCategoryScan -Categories $categories -ProcessEntity $processEntity -EntityPreFilter $entityPreFilter -ShowProgress -EntityCache $entityCache $relevantPolicies = $scanResult.Buckets # Apply scope tag filter if specified if ($ScopeTagFilter) { foreach ($key in @($relevantPolicies.Keys)) { $relevantPolicies[$key] = @(Filter-ByScopeTag -Items $relevantPolicies[$key] -FilterTag $ScopeTagFilter -ScopeTagLookup $script:ScopeTagLookup) } } # Display results Write-Host "`nAssignments for Device: $deviceName" -ForegroundColor Green # Legacy per-category name resolution. Windows 365 buckets are exported but were # never displayed by the old code, so they get no section here either. $nameFirst = { param($item) if ([string]::IsNullOrWhiteSpace($item.name)) { $item.displayName } else { $item.name } } $displayNameFirst = { param($item) if ([string]::IsNullOrWhiteSpace($item.displayName)) { $item.name } else { $item.displayName } } $displayNameOnly = { param($item) $item.displayName } $esProfileName = { param($item) if (-not [string]::IsNullOrWhiteSpace($item.displayName)) { $item.displayName } elseif (-not [string]::IsNullOrWhiteSpace($item.name)) { $item.name } else { "Unnamed Profile" } } $displaySections = @( @{ Title = 'Device Configurations'; Bucket = 'DeviceConfigs'; GetName = $nameFirst } @{ Title = 'Settings Catalog Policies'; Bucket = 'SettingsCatalog'; GetName = $nameFirst } @{ Title = 'Compliance Policies'; Bucket = 'CompliancePolicies'; GetName = $nameFirst } @{ Title = 'App Protection Policies'; Bucket = 'AppProtectionPolicies'; GetName = $displayNameOnly } @{ Title = 'App Configuration Policies'; Bucket = 'AppConfigurationPolicies'; GetName = $nameFirst } @{ Title = 'Platform Scripts'; Bucket = 'PlatformScripts'; GetName = $nameFirst } @{ Title = 'Proactive Remediation Scripts'; Bucket = 'HealthScripts'; GetName = $nameFirst } @{ Title = 'Autopilot Deployment Profiles'; Bucket = 'DeploymentProfiles'; GetName = $displayNameFirst } @{ Title = 'Enrollment Status Page Profiles'; Bucket = 'ESPProfiles'; GetName = $displayNameFirst } @{ Title = 'Required Apps'; Bucket = 'AppsRequired'; GetName = $displayNameOnly } @{ Title = 'Available Apps'; Bucket = 'AppsAvailable'; GetName = $displayNameOnly } @{ Title = 'Uninstall Apps'; Bucket = 'AppsUninstall'; GetName = $displayNameOnly } @{ Title = 'Endpoint Security - Antivirus Profiles'; Bucket = 'AntivirusProfiles'; GetName = $esProfileName } @{ Title = 'Endpoint Security - Disk Encryption Profiles'; Bucket = 'DiskEncryptionProfiles'; GetName = $esProfileName } @{ Title = 'Endpoint Security - Firewall Profiles'; Bucket = 'FirewallProfiles'; GetName = $esProfileName } @{ Title = 'Endpoint Security - EDR Profiles'; Bucket = 'EndpointDetectionProfiles'; GetName = $esProfileName } @{ Title = 'Endpoint Security - ASR Profiles'; Bucket = 'AttackSurfaceProfiles'; GetName = $esProfileName } @{ Title = 'Endpoint Security - Account Protection Profiles'; Bucket = 'AccountProtectionProfiles'; GetName = $esProfileName } ) foreach ($section in $displaySections) { Format-PolicyTable -Title $section.Title -Policies @($relevantPolicies[$section.Bucket]) -GetName $section.GetName } # Add to export data: the Device row first, then categories in the legacy CSV order # (Autopilot/ESP before Endpoint Security, Windows 365 after it, app buckets last). Add-ExportData -ExportData $exportData -Category "Device" -Items @([PSCustomObject]@{ displayName = $deviceName id = $deviceInfo.Id AssignmentReason = "N/A" }) $reasonProperty = { param($item) $item.AssignmentReason } $exportBatches = @( @{ Ids = @('DeviceConfigurations', 'SettingsCatalog', 'CompliancePolicies'); Reason = $reasonProperty } # App Protection rows export the AssignmentSummary built above @{ Ids = @('AppProtectionPolicies'); Reason = { param($item) $item.AssignmentSummary } } @{ Ids = @('AppConfigurationPolicies', 'PlatformScripts', 'HealthScripts', 'DeploymentProfiles', 'ESPProfiles', 'ESAntivirus', 'ESDiskEncryption', 'ESFirewall', 'ESEndpointDetection', 'ESAttackSurface', 'ESAccountProtection', 'CloudPCProvisioningPolicies', 'CloudPCUserSettings', 'Applications'); Reason = $reasonProperty } ) foreach ($batch in $exportBatches) { $batchCategories = foreach ($id in $batch.Ids) { $categories | Where-Object { $_.Id -eq $id } } Add-CategoryExportData -ExportData $exportData -Categories $batchCategories -Buckets $relevantPolicies -AssignmentReason $batch.Reason } } # Export results if requested Export-ResultsIfRequested -ExportData $exportData -DefaultFileName "IntuneDeviceAssignments.csv" -ForceExport:$ExportToCSV -CustomExportPath $ExportPath -ExportToCSV:$ExportToCSV -ParameterMode:$parameterMode } |