Public/Get-IntuneEffectiveAssignment.ps1
|
function Get-IntuneEffectiveAssignment { <# .SYNOPSIS Explains effective Intune assignment targeting for a user, a managed device, or both. .DESCRIPTION Combines All Users, All Devices, transitive user/device group membership, group exclusions, and assignment-filter evaluation. Results are Included, Excluded, NotTargeted, or Unknown with a machine-readable ReasonChain. This command explains assignment targeting only. It does not prove delivery, platform applicability, installation, execution, compliance, or device check-in. .PARAMETER UserPrincipalName Optional user principal name. Supply a user, a managed device, or both. .PARAMETER DeviceName Optional Intune managed-device name or managed-device ID. .PARAMETER PassThru Returns IntuneAssignmentChecker.AssignmentRecord objects with EffectiveState and ReasonChain fields. .PARAMETER ExportToCSV Exports effective results, including a compact JSON reason chain, to CSV. .PARAMETER ExportPath CSV destination. The path must end in .csv. Supplying it enables export. .EXAMPLE Get-IntuneEffectiveAssignment -UserPrincipalName 'user@contoso.com' .EXAMPLE Get-IntuneEffectiveAssignment -UserPrincipalName 'user@contoso.com' -DeviceName 'WIN-01' -PassThru .EXAMPLE Get-IntuneEffectiveAssignment -DeviceName 'WIN-01' -ExportPath './effective.csv' #> [CmdletBinding()] [OutputType('IntuneAssignmentChecker.AssignmentRecord')] param( [Parameter()] [string]$UserPrincipalName, [Parameter()] [string]$DeviceName, [Parameter()] [switch]$PassThru, [Parameter()] [switch]$ExportToCSV, [Parameter()] [string]$ExportPath ) if ([string]::IsNullOrWhiteSpace($UserPrincipalName) -and [string]::IsNullOrWhiteSpace($DeviceName)) { Write-Error 'Supply -UserPrincipalName, -DeviceName, or both.' return } if ([string]::IsNullOrWhiteSpace($script:GraphEndpoint)) { Write-Error 'Connect first with Connect-IntuneAssignmentChecker.' return } if (-not [string]::IsNullOrWhiteSpace($ExportPath) -and [System.IO.Path]::GetExtension($ExportPath) -ine '.csv') { Write-Error 'ExportPath must be a .csv file.' return } $userInfo = $null $managedDevice = $null $directoryDevice = $null if (-not [string]::IsNullOrWhiteSpace($UserPrincipalName)) { $userInfo = Get-UserInfo -UserPrincipalName $UserPrincipalName.Trim() if (-not $userInfo.Success) { Write-Error "User '$UserPrincipalName' was not found." return } } if (-not [string]::IsNullOrWhiteSpace($DeviceName)) { $managedDeviceResult = Get-IACManagedDevice -Identity $DeviceName.Trim() if (-not $managedDeviceResult.Success) { Write-Error $managedDeviceResult.Reason return } $managedDevice = $managedDeviceResult.Device if (-not [string]::IsNullOrWhiteSpace("$($managedDevice.azureADDeviceId)")) { $directoryResult = Get-IACDirectoryDevice -AzureADDeviceId "$($managedDevice.azureADDeviceId)" if ($directoryResult.Success) { $directoryDevice = $directoryResult.Device } else { Write-Warning $directoryResult.Reason } } else { Write-Warning 'The managed device has no azureADDeviceId; device group targeting will remain Unknown.' } } if ($null -eq $script:AssignmentFilterLookup) { $script:AssignmentFilterLookup = Get-AssignmentFilterLookup } $membershipSources = @{} $userMembershipKnown = $true $deviceMembershipKnown = $true if ($userInfo) { try { foreach ($group in @(Get-GroupMemberships -ObjectId $userInfo.Id -ObjectType User)) { if (-not $group.id) { continue } $key = "$($group.id)" if (-not $membershipSources.ContainsKey($key)) { $membershipSources[$key] = [PSCustomObject]@{ Name = $group.displayName; Sources = [System.Collections.Generic.List[string]]::new() } } if (-not $membershipSources[$key].Sources.Contains('User')) { [void]$membershipSources[$key].Sources.Add('User') } } } catch { $userMembershipKnown = $false Write-Warning "User transitive group membership is incomplete: $($_.Exception.Message)" } } if ($managedDevice) { if ($directoryDevice) { try { foreach ($group in @(Get-GroupMemberships -ObjectId $directoryDevice.id -ObjectType Device)) { if (-not $group.id) { continue } $key = "$($group.id)" if (-not $membershipSources.ContainsKey($key)) { $membershipSources[$key] = [PSCustomObject]@{ Name = $group.displayName; Sources = [System.Collections.Generic.List[string]]::new() } } if (-not $membershipSources[$key].Sources.Contains('Device')) { [void]$membershipSources[$key].Sources.Add('Device') } } } catch { $deviceMembershipKnown = $false Write-Warning "Device transitive group membership is incomplete: $($_.Exception.Message)" } } else { $deviceMembershipKnown = $false } } $subjectType = if ($userInfo -and $managedDevice) { 'UserDevice' } elseif ($userInfo) { 'User' } else { 'Device' } $subjectId = if ($userInfo -and $managedDevice) { "$($userInfo.Id)|$($managedDevice.id)" } elseif ($userInfo) { "$($userInfo.Id)" } else { "$($managedDevice.id)" } $subjectName = if ($userInfo -and $managedDevice) { "$($userInfo.UserPrincipalName) on $($managedDevice.deviceName)" } elseif ($userInfo) { "$($userInfo.UserPrincipalName)" } else { "$($managedDevice.deviceName)" } $effectiveRecords = [System.Collections.Generic.List[object]]::new() $categories = Get-IntuneCategoryDefinition -Audience Effective $entityCache = @{} $processEntity = { param($context) $assignmentSets = if ($context.Category.Id -eq 'Applications' -and $context.Assignments.Count -gt 0) { @($context.Assignments | Group-Object { if ([string]::IsNullOrWhiteSpace("$($_.Intent)")) { 'none' } else { "$($_.Intent)".ToLowerInvariant() } } | ForEach-Object { [PSCustomObject]@{ Intent = $_.Name; Assignments = @($_.Group) } }) } else { @([PSCustomObject]@{ Intent = $null; Assignments = @($context.Assignments) }) } foreach ($assignmentSet in $assignmentSets) { $effectiveCategory = $context.Category.PSObject.Copy() if ($context.Category.Id -eq 'Applications') { $effectiveCategory.ExportCategory = switch ($assignmentSet.Intent) { 'required' { 'Required App' } 'available' { 'Available App' } 'uninstall' { 'Uninstall App' } default { 'Application' } } } $record = Resolve-IACEffectiveAssignment -Category $effectiveCategory -Entity $context.Entity ` -Assignments $assignmentSet.Assignments -MembershipSources $membershipSources ` -HasUser:($null -ne $userInfo) -HasDevice:($null -ne $managedDevice) ` -UserMembershipKnown $userMembershipKnown -DeviceMembershipKnown $deviceMembershipKnown ` -ManagedDevice $managedDevice -SubjectType $subjectType -SubjectId $subjectId -SubjectName $subjectName [void]$effectiveRecords.Add($record) } } $scan = Invoke-IntuneCategoryScan -Categories $categories -ProcessEntity $processEntity ` -EntityCache $entityCache -ShowProgress -ProgressVerb 'Evaluating' if ($scan.Errors.Count -gt 0) { foreach ($scanError in $scan.Errors) { $failedCategory = $categories | Where-Object Id -eq $scanError.CategoryId | Select-Object -First 1 $categoryName = if ($failedCategory.ExportCategory) { $failedCategory.ExportCategory } elseif ($scanError.DisplayName) { $scanError.DisplayName } else { $scanError.CategoryId } $failureReason = [PSCustomObject][ordered]@{ Sequence = 1 Code = 'Scan.CategoryFailed' FilterCode = $null Outcome = 'Unknown' AssignmentId = $null AssignmentMode = 'Unknown' TargetType = 'Unknown' TargetId = $null MembershipSources = @() TargetResult = 'Unknown' FilterId = $null FilterMode = $null FilterResult = 'Unknown' Message = "$($scanError.Message)" } $failureRecord = New-IACAssignmentRecord ` -CategoryId "$($scanError.CategoryId)" -Category "$categoryName" ` -PolicyId '' -PolicyName '[Category scan failed]' ` -Platform $(if ($failedCategory.Platform) { $failedCategory.Platform } else { 'Unknown' }) ` -AssignmentMode Unknown -TargetType Unknown -EffectiveState Unknown ` -ReasonChain @($failureReason) -SubjectType $subjectType -SubjectId $subjectId -SubjectName $subjectName ` -AssignmentReason "$($scanError.Message)" -Source 'Get-IntuneEffectiveAssignment' [void]$effectiveRecords.Add($failureRecord) Write-Warning "Category '$($scanError.CategoryId)' failed: $($scanError.Message)" } } if (-not $PassThru) { if ($effectiveRecords.Count -gt 0) { $table = $effectiveRecords | Select-Object PolicyName, Category, EffectiveState, AssignmentMode, TargetType, TargetName | Format-Table -AutoSize | Out-String Write-Host $table } foreach ($state in @('Included', 'Excluded', 'Unknown', 'NotTargeted')) { $count = @($effectiveRecords | Where-Object EffectiveState -eq $state).Count Write-Host "$state`: $count" } Write-Host 'Targeting analysis is not proof of delivery, applicability, installation, execution, compliance, or device check-in.' -ForegroundColor Yellow } else { Write-Verbose 'Targeting analysis is not proof of delivery, applicability, installation, execution, compliance, or device check-in.' } if ($ExportToCSV -or -not [string]::IsNullOrWhiteSpace($ExportPath)) { $csvPath = if ([string]::IsNullOrWhiteSpace($ExportPath)) { Join-Path (Get-Location) 'IntuneEffectiveAssignments.csv' } else { $ExportPath } $parentPath = Split-Path -Parent $csvPath if ($parentPath -and -not (Test-Path $parentPath)) { New-Item -ItemType Directory -Path $parentPath -Force | Out-Null } $csvRows = foreach ($record in $effectiveRecords) { [PSCustomObject][ordered]@{ SubjectType = ConvertTo-IACCsvSafeValue $record.SubjectType SubjectId = ConvertTo-IACCsvSafeValue $record.SubjectId SubjectName = ConvertTo-IACCsvSafeValue $record.SubjectName CategoryId = ConvertTo-IACCsvSafeValue $record.CategoryId Category = ConvertTo-IACCsvSafeValue $record.Category PolicyName = ConvertTo-IACCsvSafeValue $record.PolicyName PolicyId = ConvertTo-IACCsvSafeValue $record.PolicyId Platform = ConvertTo-IACCsvSafeValue $record.Platform Intent = ConvertTo-IACCsvSafeValue $record.Intent EffectiveState = $record.EffectiveState AssignmentMode = $record.AssignmentMode TargetType = $record.TargetType TargetId = ConvertTo-IACCsvSafeValue $record.TargetId TargetName = ConvertTo-IACCsvSafeValue $record.TargetName FilterId = ConvertTo-IACCsvSafeValue $record.FilterId FilterName = ConvertTo-IACCsvSafeValue $record.FilterName FilterMode = $record.FilterMode AssignmentReason = ConvertTo-IACCsvSafeValue $record.AssignmentReason DecisionCode = if ($record.ReasonChain.Count -gt 0) { $record.ReasonChain[-1].Code } else { $null } ReasonChain = ConvertTo-IACCsvSafeValue (ConvertTo-Json -InputObject @($record.ReasonChain) -Depth 6 -Compress -AsArray) } } $csvRows | Export-Csv -Path $csvPath -NoTypeInformation -Encoding utf8 if ($PassThru) { Write-Verbose "Results exported to $csvPath" } else { Write-Host "Results exported to $csvPath" -ForegroundColor Green } } if ($PassThru) { $effectiveRecords } } |