Public/Get-IntuneUserDeviceAssignment.ps1

function Get-IntuneUserDeviceAssignment {
    <#
    .SYNOPSIS
    Shows the full effective set of Intune policies and apps that would apply to a specific User on a specific Device.

    .DESCRIPTION
    Combines the user's group memberships with the device's group memberships and evaluates every policy/app
    category as if both subjects were present at the same time. This mirrors what Intune actually does for an
    Autopilot deployment where User X is assigned to Device Y, without requiring you to test by reprovisioning.

    Assignment filters (Include/Exclude) are listed but their rule expressions are not evaluated against device
    properties. Filter evaluation is performed by the Intune service itself at deployment time.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false, HelpMessage = "User Principal Name")]
        [string]$UserPrincipalName,

        [Parameter(Mandatory = $false, HelpMessage = "Device display name or Object ID")]
        [string]$DeviceName,

        [Parameter(Mandatory = $false)]
        [switch]$ExportToCSV,

        [Parameter(Mandatory = $false)]
        [string]$ExportPath,

        [Parameter(Mandatory = $false)]
        [string]$ScopeTagFilter
    )

    Write-Host "What-If: User on Device - effective policy preview" -ForegroundColor Green

    # ── Resolve User and Device ──────────────────────────────────────────────
    if ([string]::IsNullOrWhiteSpace($UserPrincipalName)) {
        Write-Host "Please enter the User Principal Name: " -NoNewline -ForegroundColor Cyan
        $UserPrincipalName = Read-Host
    }
    if ([string]::IsNullOrWhiteSpace($DeviceName)) {
        Write-Host "Please enter the Device name: " -NoNewline -ForegroundColor Cyan
        $DeviceName = Read-Host
    }

    if ([string]::IsNullOrWhiteSpace($UserPrincipalName) -or [string]::IsNullOrWhiteSpace($DeviceName)) {
        Write-Host "Both a User Principal Name and a Device name are required." -ForegroundColor Red
        return
    }

    $upn = ($UserPrincipalName -split ',')[0].Trim()
    if ($upn -notmatch '^[^@\s]+@[^@\s]+\.[^@\s]+$') {
        Write-Host "Invalid UPN format: '$upn'. Expected: user@domain.com" -ForegroundColor Red
        return
    }
    $devName = ($DeviceName -split ',')[0].Trim()

    Write-Host "Looking up user: $upn" -ForegroundColor Yellow
    $userInfo = Get-UserInfo -UserPrincipalName $upn
    if (-not $userInfo.Success) {
        Write-Host "User not found: $upn" -ForegroundColor Red
        return
    }

    Write-Host "Looking up device: $devName" -ForegroundColor Yellow
    $deviceInfo = $null
    if ($devName -match '^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$') {
        try {
            $selectProps = "id,displayName,operatingSystem,operatingSystemVersion"
            $directDevice = Invoke-MgGraphRequest -Uri "$script:GraphEndpoint/beta/devices/$($devName)?`$select=$selectProps" -Method Get
            $deviceInfo = @{
                Id              = $directDevice.id
                DisplayName     = $directDevice.displayName
                OperatingSystem = $directDevice.operatingSystem
                Success         = $true
                MultipleFound   = $false
            }
        }
        catch {
            Write-Host "No device found with Object ID: $devName" -ForegroundColor Red
            return
        }
    }
    else {
        $deviceInfo = Get-DeviceInfo -DeviceName $devName
        if (-not $deviceInfo.Success) {
            Write-Host "Device not found: $devName" -ForegroundColor Red
            return
        }
        if ($deviceInfo.MultipleFound) {
            Write-Host "Multiple devices match name '$devName'. Use the Object ID instead:" -ForegroundColor Red
            foreach ($d in $deviceInfo.AllDevices) {
                Write-Host " - $($d.displayName) (ID: $($d.id), OS: $($d.operatingSystem))" -ForegroundColor Yellow
            }
            return
        }
    }

    Write-Host "Resolved: $($userInfo.UserPrincipalName) + $($deviceInfo.DisplayName) (OS: $($deviceInfo.OperatingSystem))" -ForegroundColor Green

    # ── Group memberships ────────────────────────────────────────────────────
    $userGroupIds   = @()
    $deviceGroupIds = @()
    try {
        $uGroups = Get-GroupMemberships -ObjectId $userInfo.Id   -ObjectType "User"
        $dGroups = Get-GroupMemberships -ObjectId $deviceInfo.Id -ObjectType "Device"
        $userGroupIds   = @($uGroups | Where-Object { $_.id } | ForEach-Object { $_.id })
        $deviceGroupIds = @($dGroups | Where-Object { $_.id } | ForEach-Object { $_.id })
    }
    catch {
        Write-Host "Error fetching group memberships: $($_.Exception.Message)" -ForegroundColor Red
        return
    }
    $combinedGroupIds = @(($userGroupIds + $deviceGroupIds) | Select-Object -Unique)
    $deviceOS = $deviceInfo.OperatingSystem
    $includeReasons = @("All Users", "All Devices")

    # ── Helper: classify the source of a winning assignment ──────────────────
    # Returns @{ Reason = '...'; Source = 'All Users'|'All Devices'|'User group'|'Device group'|'User+Device group'|'Excluded' }
    $classify = {
        param($assignments, $reasonString)

        if (-not $reasonString) { return $null }

        $source = if ($reasonString -like 'Excluded*')    { 'Excluded' }
                  elseif ($reasonString -like 'All Users*')   { 'All Users' }
                  elseif ($reasonString -like 'All Devices*') { 'All Devices' }
                  else { $null }

        if (-not $source) {
            # Group Assignment - find the matching assignment to learn its GroupId
            foreach ($a in $assignments) {
                if ($a.Reason -eq 'Group Assignment') {
                    $inUser   = $userGroupIds   -contains $a.GroupId
                    $inDevice = $deviceGroupIds -contains $a.GroupId
                    if ($inUser -and $inDevice) { $source = 'User+Device group'; break }
                    elseif ($inUser)            { $source = 'User group';        break }
                    elseif ($inDevice)          { $source = 'Device group';      break }
                }
            }
            if (-not $source) { $source = 'Group' }
        }

        return @{ Reason = $reasonString; Source = $source }
    }

    Write-Host "Fetching effective policies and apps..." -ForegroundColor Yellow

    $relevantPolicies = @{
        DeviceConfigs               = [System.Collections.ArrayList]::new()
        SettingsCatalog             = [System.Collections.ArrayList]::new()
        CompliancePolicies          = [System.Collections.ArrayList]::new()
        AppProtectionPolicies       = [System.Collections.ArrayList]::new()
        AppConfigurationPolicies    = [System.Collections.ArrayList]::new()
        AppsRequired                = [System.Collections.ArrayList]::new()
        AppsAvailable               = [System.Collections.ArrayList]::new()
        AppsUninstall               = [System.Collections.ArrayList]::new()
        PlatformScripts             = [System.Collections.ArrayList]::new()
        HealthScripts               = [System.Collections.ArrayList]::new()
        AntivirusProfiles           = [System.Collections.ArrayList]::new()
        DiskEncryptionProfiles      = [System.Collections.ArrayList]::new()
        FirewallProfiles            = [System.Collections.ArrayList]::new()
        EndpointDetectionProfiles   = [System.Collections.ArrayList]::new()
        AttackSurfaceProfiles       = [System.Collections.ArrayList]::new()
        AccountProtectionProfiles   = [System.Collections.ArrayList]::new()
        DeploymentProfiles          = [System.Collections.ArrayList]::new()
        ESPProfiles                 = [System.Collections.ArrayList]::new()
        CloudPCProvisioningPolicies = [System.Collections.ArrayList]::new()
        CloudPCUserSettings         = [System.Collections.ArrayList]::new()
    }

    # Helper: standard fetch -> resolve -> classify pattern for generic categories
    $processGeneric = {
        param($entityType, $bucketKey, [switch]$SkipPlatformCheck)

        $items = Get-IntuneEntities -EntityType $entityType
        foreach ($item in $items) {
            $assignments = Get-IntuneAssignments -EntityType $entityType -EntityId $item.id
            $reason = Resolve-AssignmentReason -Assignments $assignments -GroupMembershipIds $combinedGroupIds -IncludeReasons $includeReasons
            if (-not $reason) { continue }
            if (-not $SkipPlatformCheck -and -not (Test-PlatformCompatibility -DeviceOS $deviceOS -Policy $item)) { continue }
            $info = & $classify $assignments $reason
            $item | Add-Member -NotePropertyName 'AssignmentReason' -NotePropertyValue $info.Reason -Force
            $item | Add-Member -NotePropertyName 'Source'           -NotePropertyValue $info.Source -Force
            [void]$relevantPolicies[$bucketKey].Add($item)
        }
    }

    # Helper: ES intents-based fetch
    $processIntent = {
        param($templateFamily, $bucketKey, $processedSet)

        # configurationPolicies branch
        $configPolicies = Get-IntuneEntities -EntityType "configurationPolicies"
        $matching = $configPolicies | Where-Object { $_.templateReference -and $_.templateReference.templateFamily -eq $templateFamily }
        foreach ($policy in $matching) {
            if (-not $processedSet.Add($policy.id)) { continue }
            $assignments = Get-IntuneAssignments -EntityType "configurationPolicies" -EntityId $policy.id
            $reason = Resolve-AssignmentReason -Assignments $assignments -GroupMembershipIds $combinedGroupIds -IncludeReasons $includeReasons
            if (-not $reason) { continue }
            if (-not (Test-PlatformCompatibility -DeviceOS $deviceOS -Policy $policy)) { continue }
            $info = & $classify $assignments $reason
            $policy | Add-Member -NotePropertyName 'AssignmentReason' -NotePropertyValue $info.Reason -Force
            $policy | Add-Member -NotePropertyName 'Source'           -NotePropertyValue $info.Source -Force
            [void]$relevantPolicies[$bucketKey].Add($policy)
        }

        # deviceManagement/intents branch
        $intents = Get-IntuneEntities -EntityType "deviceManagement/intents"
        Add-IntentTemplateFamilyInfo -IntentPolicies $intents
        $matchingIntents = $intents | Where-Object { $_.templateReference -and $_.templateReference.templateFamily -eq $templateFamily }
        foreach ($policy in $matchingIntents) {
            if (-not $processedSet.Add($policy.id)) { continue }
            $resp = Invoke-MgGraphRequest -Uri "$script:GraphEndpoint/beta/deviceManagement/intents/$($policy.id)/assignments" -Method Get
            $assignmentList = foreach ($a in $resp.value) {
                [PSCustomObject]@{
                    Reason  = switch ($a.target.'@odata.type') {
                        '#microsoft.graph.allLicensedUsersAssignmentTarget' { "All Users" }
                        '#microsoft.graph.allDevicesAssignmentTarget'      { "All Devices" }
                        '#microsoft.graph.groupAssignmentTarget'           { "Group Assignment" }
                        '#microsoft.graph.exclusionGroupAssignmentTarget'  { "Group Exclusion" }
                        default { "Unknown" }
                    }
                    GroupId = if ($a.target.'@odata.type' -match "groupAssignmentTarget") { $a.target.groupId } else { $null }
                }
            }
            $reason = Resolve-AssignmentReason -Assignments $assignmentList -GroupMembershipIds $combinedGroupIds -IncludeReasons $includeReasons
            if (-not $reason) { continue }
            if (-not (Test-PlatformCompatibility -DeviceOS $deviceOS -Policy $policy)) { continue }
            $info = & $classify $assignmentList $reason
            $policy | Add-Member -NotePropertyName 'AssignmentReason' -NotePropertyValue $info.Reason -Force
            $policy | Add-Member -NotePropertyName 'Source'           -NotePropertyValue $info.Source -Force
            [void]$relevantPolicies[$bucketKey].Add($policy)
        }
    }

    # ── Generic categories ───────────────────────────────────────────────────
    Write-Host " Device Configurations..." -ForegroundColor Yellow
    & $processGeneric "deviceConfigurations" "DeviceConfigs"

    Write-Host " Settings Catalog Policies..." -ForegroundColor Yellow
    $allConfigPolicies = Get-IntuneEntities -EntityType "configurationPolicies"
    foreach ($policy in $allConfigPolicies) {
        # Skip endpoint-security templates - they're handled separately
        if ($policy.templateReference -and $policy.templateReference.templateFamily -like 'endpointSecurity*') { continue }
        $assignments = Get-IntuneAssignments -EntityType "configurationPolicies" -EntityId $policy.id
        $reason = Resolve-AssignmentReason -Assignments $assignments -GroupMembershipIds $combinedGroupIds -IncludeReasons $includeReasons
        if (-not $reason) { continue }
        if (-not (Test-PlatformCompatibility -DeviceOS $deviceOS -Policy $policy)) { continue }
        $info = & $classify $assignments $reason
        $policy | Add-Member -NotePropertyName 'AssignmentReason' -NotePropertyValue $info.Reason -Force
        $policy | Add-Member -NotePropertyName 'Source'           -NotePropertyValue $info.Source -Force
        [void]$relevantPolicies.SettingsCatalog.Add($policy)
    }

    Write-Host " Compliance Policies..." -ForegroundColor Yellow
    & $processGeneric "deviceCompliancePolicies" "CompliancePolicies"

    Write-Host " App Configuration Policies..." -ForegroundColor Yellow
    $appConfigs = Get-IntuneEntities -EntityType "deviceAppManagement/mobileAppConfigurations"
    foreach ($policy in $appConfigs) {
        $assignments = Get-IntuneAssignments -EntityType "mobileAppConfigurations" -EntityId $policy.id
        $reason = Resolve-AssignmentReason -Assignments $assignments -GroupMembershipIds $combinedGroupIds -IncludeReasons $includeReasons
        if (-not $reason) { continue }
        if (-not (Test-PlatformCompatibility -DeviceOS $deviceOS -Policy $policy)) { continue }
        $info = & $classify $assignments $reason
        $policy | Add-Member -NotePropertyName 'AssignmentReason' -NotePropertyValue $info.Reason -Force
        $policy | Add-Member -NotePropertyName 'Source'           -NotePropertyValue $info.Source -Force
        [void]$relevantPolicies.AppConfigurationPolicies.Add($policy)
    }

    Write-Host " Platform Scripts..." -ForegroundColor Yellow
    & $processGeneric "deviceManagementScripts" "PlatformScripts"

    Write-Host " Proactive Remediation Scripts..." -ForegroundColor Yellow
    & $processGeneric "deviceHealthScripts" "HealthScripts"

    if (-not $deviceOS -or $deviceOS -eq "Windows") {
        Write-Host " Autopilot Deployment Profiles..." -ForegroundColor Yellow
        & $processGeneric "windowsAutopilotDeploymentProfiles" "DeploymentProfiles" -SkipPlatformCheck

        Write-Host " Enrollment Status Page Profiles..." -ForegroundColor Yellow
        $enrollmentConfigs = Get-IntuneEntities -EntityType "deviceEnrollmentConfigurations"
        $espProfiles = $enrollmentConfigs | Where-Object { $_.'@odata.type' -match 'EnrollmentCompletionPageConfiguration' }
        foreach ($esp in $espProfiles) {
            $assignments = Get-IntuneAssignments -EntityType "deviceEnrollmentConfigurations" -EntityId $esp.id
            $reason = Resolve-AssignmentReason -Assignments $assignments -GroupMembershipIds $combinedGroupIds -IncludeReasons $includeReasons
            if (-not $reason) { continue }
            $info = & $classify $assignments $reason
            $esp | Add-Member -NotePropertyName 'AssignmentReason' -NotePropertyValue $info.Reason -Force
            $esp | Add-Member -NotePropertyName 'Source'           -NotePropertyValue $info.Source -Force
            [void]$relevantPolicies.ESPProfiles.Add($esp)
        }

        Write-Host " Cloud PC Provisioning / User Settings..." -ForegroundColor Yellow
        try {
            & $processGeneric "virtualEndpoint/provisioningPolicies" "CloudPCProvisioningPolicies" -SkipPlatformCheck
            & $processGeneric "virtualEndpoint/userSettings" "CloudPCUserSettings" -SkipPlatformCheck
        }
        catch { Write-Verbose "Skipping - Windows 365 may not be licensed for this tenant" }
    }

    # ── App Protection (per-platform endpoints, user-targeted only) ──────────
    Write-Host " App Protection Policies..." -ForegroundColor Yellow
    $appProt = Get-IntuneEntities -EntityType "deviceAppManagement/managedAppPolicies"
    foreach ($policy in $appProt) {
        if (-not (Test-PlatformCompatibility -DeviceOS $deviceOS -Policy $policy)) { continue }
        $assignmentsUri = switch ($policy.'@odata.type') {
            "#microsoft.graph.androidManagedAppProtection" { "$script:GraphEndpoint/beta/deviceAppManagement/androidManagedAppProtections('$($policy.id)')/assignments" }
            "#microsoft.graph.iosManagedAppProtection"     { "$script:GraphEndpoint/beta/deviceAppManagement/iosManagedAppProtections('$($policy.id)')/assignments" }
            "#microsoft.graph.windowsManagedAppProtection" { "$script:GraphEndpoint/beta/deviceAppManagement/windowsManagedAppProtections('$($policy.id)')/assignments" }
            default { $null }
        }
        if (-not $assignmentsUri) { continue }
        try {
            $resp = Invoke-MgGraphRequest -Uri $assignmentsUri -Method Get
            $assignmentList = foreach ($a in $resp.value) {
                [PSCustomObject]@{
                    Reason  = switch ($a.target.'@odata.type') {
                        '#microsoft.graph.allLicensedUsersAssignmentTarget' { "All Users" }
                        '#microsoft.graph.groupAssignmentTarget'           { "Group Assignment" }
                        '#microsoft.graph.exclusionGroupAssignmentTarget'  { "Group Exclusion" }
                        default { "Unknown" }
                    }
                    GroupId = if ($a.target.'@odata.type' -match "groupAssignmentTarget") { $a.target.groupId } else { $null }
                }
            }
            $reason = Resolve-AssignmentReason -Assignments $assignmentList -GroupMembershipIds $combinedGroupIds -IncludeReasons $includeReasons
            if (-not $reason) { continue }
            $info = & $classify $assignmentList $reason
            $policy | Add-Member -NotePropertyName 'AssignmentReason' -NotePropertyValue $info.Reason -Force
            $policy | Add-Member -NotePropertyName 'Source'           -NotePropertyValue $info.Source -Force
            [void]$relevantPolicies.AppProtectionPolicies.Add($policy)
        }
        catch {
            Write-Host "Error fetching assignments for App Protection $($policy.displayName): $($_.Exception.Message)" -ForegroundColor Red
        }
    }

    # ── Endpoint Security categories (templated) ─────────────────────────────
    $esFamilies = @(
        @{ Family = 'endpointSecurityAntivirus';                    Bucket = 'AntivirusProfiles'         }
        @{ Family = 'endpointSecurityDiskEncryption';               Bucket = 'DiskEncryptionProfiles'    }
        @{ Family = 'endpointSecurityFirewall';                     Bucket = 'FirewallProfiles'          }
        @{ Family = 'endpointSecurityEndpointDetectionAndResponse'; Bucket = 'EndpointDetectionProfiles' }
        @{ Family = 'endpointSecurityAttackSurfaceReduction';       Bucket = 'AttackSurfaceProfiles'     }
        @{ Family = 'endpointSecurityAccountProtection';            Bucket = 'AccountProtectionProfiles' }
    )
    foreach ($f in $esFamilies) {
        Write-Host " Endpoint Security: $($f.Family)..." -ForegroundColor Yellow
        $processed = [System.Collections.Generic.HashSet[string]]::new()
        & $processIntent $f.Family $f.Bucket $processed
    }

    # ── Applications ─────────────────────────────────────────────────────────
    Write-Host " Applications..." -ForegroundColor Yellow
    $appUri = "$script:GraphEndpoint/beta/deviceAppManagement/mobileApps?`$filter=isAssigned eq true"
    $appResponse = Invoke-MgGraphRequest -Uri $appUri -Method Get
    $allApps = $appResponse.value
    while ($appResponse.'@odata.nextLink') {
        $appResponse = Invoke-MgGraphRequest -Uri $appResponse.'@odata.nextLink' -Method Get
        $allApps += $appResponse.value
    }

    foreach ($app in $allApps) {
        if ($app.isFeatured -or $app.isBuiltIn) { continue }
        if (-not (Test-AppPlatformCompatibility -DeviceOS $deviceOS -App $app)) { continue }

        try {
            $assignmentsUri = "$script:GraphEndpoint/beta/deviceAppManagement/mobileApps('$($app.id)')/assignments"
            $resp = Invoke-MgGraphRequest -Uri $assignmentsUri -Method Get

            # Single pass: capture exclusion membership, the winning include, and the intent.
            # We need the intent from an inclusion to know which app bucket to route into,
            # even when an exclusion ultimately wins.
            $isExcluded = $false
            $excludingTarget = $null
            $winningIncludeTarget = $null
            $winningIncludeReason = $null
            $winningIncludeGroupId = $null
            $intent = $null

            foreach ($a in $resp.value) {
                $t = $a.target.'@odata.type'
                $g = $a.target.groupId

                if ($t -eq '#microsoft.graph.exclusionGroupAssignmentTarget' -and $combinedGroupIds -contains $g) {
                    if (-not $isExcluded) { $excludingTarget = $a.target }
                    $isExcluded = $true
                    continue
                }

                # Inclusion paths
                $isIncludeMatch = $false
                $matchReason = $null
                if ($t -eq '#microsoft.graph.allLicensedUsersAssignmentTarget') {
                    $isIncludeMatch = $true; $matchReason = "All Users"
                }
                elseif ($t -eq '#microsoft.graph.allDevicesAssignmentTarget') {
                    $isIncludeMatch = $true; $matchReason = "All Devices"
                }
                elseif ($t -eq '#microsoft.graph.groupAssignmentTarget' -and $combinedGroupIds -contains $g) {
                    $isIncludeMatch = $true; $matchReason = "Group Assignment"
                }

                if ($isIncludeMatch) {
                    if (-not $intent) { $intent = $a.intent }
                    if (-not $winningIncludeTarget) {
                        $winningIncludeTarget  = $a.target
                        $winningIncludeReason  = $matchReason
                        $winningIncludeGroupId = if ($matchReason -eq "Group Assignment") { $g } else { $null }
                    }
                }
            }

            # If neither an exclusion nor an inclusion applies, skip this app entirely.
            if (-not $isExcluded -and -not $winningIncludeTarget) { continue }
            # If exclusion applies but no inclusion would have matched, the exclusion is irrelevant.
            if ($isExcluded -and -not $winningIncludeTarget) { continue }

            if ($isExcluded) {
                $suffix = Format-AssignmentFilter -FilterId $excludingTarget.deviceAndAppManagementAssignmentFilterId -FilterType $excludingTarget.deviceAndAppManagementAssignmentFilterType
                $appCopy = $app.PSObject.Copy()
                $appCopy | Add-Member -NotePropertyName 'AssignmentReason' -NotePropertyValue "Excluded$suffix" -Force
                $appCopy | Add-Member -NotePropertyName 'Source'           -NotePropertyValue 'Excluded' -Force
                $appCopy | Add-Member -NotePropertyName 'AssignmentIntent' -NotePropertyValue $intent -Force
            }
            else {
                $suffix = Format-AssignmentFilter -FilterId $winningIncludeTarget.deviceAndAppManagementAssignmentFilterId -FilterType $winningIncludeTarget.deviceAndAppManagementAssignmentFilterType
                $source = if ($winningIncludeReason -eq "All Users") { 'All Users' }
                          elseif ($winningIncludeReason -eq "All Devices") { 'All Devices' }
                          else {
                              $inUser   = $userGroupIds   -contains $winningIncludeGroupId
                              $inDevice = $deviceGroupIds -contains $winningIncludeGroupId
                              if ($inUser -and $inDevice) { 'User+Device group' }
                              elseif ($inUser)            { 'User group' }
                              elseif ($inDevice)          { 'Device group' }
                              else                        { 'Group' }
                          }
                $appCopy = $app.PSObject.Copy()
                $appCopy | Add-Member -NotePropertyName 'AssignmentReason' -NotePropertyValue "$winningIncludeReason$suffix" -Force
                $appCopy | Add-Member -NotePropertyName 'Source'           -NotePropertyValue $source -Force
                $appCopy | Add-Member -NotePropertyName 'AssignmentIntent' -NotePropertyValue $intent -Force
            }

            switch ($intent) {
                "required"  { [void]$relevantPolicies.AppsRequired.Add($appCopy) }
                "available" { [void]$relevantPolicies.AppsAvailable.Add($appCopy) }
                "uninstall" { [void]$relevantPolicies.AppsUninstall.Add($appCopy) }
            }
        }
        catch {
            Write-Host "Error fetching assignments for app $($app.displayName): $($_.Exception.Message)" -ForegroundColor Red
        }
    }

    # ── Apply scope tag filter ───────────────────────────────────────────────
    if ($ScopeTagFilter) {
        foreach ($key in @($relevantPolicies.Keys)) {
            $relevantPolicies[$key] = @(Filter-ByScopeTag -Items $relevantPolicies[$key] -FilterTag $ScopeTagFilter -ScopeTagLookup $script:ScopeTagLookup)
        }
    }

    # ── Display ──────────────────────────────────────────────────────────────
    Write-Host ""
    Write-Host (Get-Separator -Character "=") -ForegroundColor Yellow
    Write-Host " WHAT-IF: User on Device - Effective Policies" -ForegroundColor Yellow
    Write-Host (Get-Separator -Character "=") -ForegroundColor Yellow
    Write-Host " User: $upn" -ForegroundColor White
    Write-Host " Device: $($deviceInfo.DisplayName) (OS: $deviceOS)" -ForegroundColor White
    Write-Host " Note: Assignment filter rules are listed but not evaluated against device properties." -ForegroundColor DarkGray
    Write-Host (Get-Separator -Character "=") -ForegroundColor Yellow

    $categoryDisplay = [ordered]@{
        DeviceConfigs               = "Device Configurations"
        SettingsCatalog             = "Settings Catalog Policies"
        CompliancePolicies          = "Compliance Policies"
        AppProtectionPolicies       = "App Protection Policies"
        AppConfigurationPolicies    = "App Configuration Policies"
        AppsRequired                = "Required Apps"
        AppsAvailable               = "Available Apps"
        AppsUninstall               = "Uninstall Apps"
        PlatformScripts             = "Platform Scripts"
        HealthScripts               = "Proactive Remediation Scripts"
        AntivirusProfiles           = "Endpoint Security - Antivirus"
        DiskEncryptionProfiles      = "Endpoint Security - Disk Encryption"
        FirewallProfiles            = "Endpoint Security - Firewall"
        EndpointDetectionProfiles   = "Endpoint Security - EDR"
        AttackSurfaceProfiles       = "Endpoint Security - ASR"
        AccountProtectionProfiles   = "Endpoint Security - Account Protection"
        DeploymentProfiles          = "Autopilot Deployment Profiles"
        ESPProfiles                 = "Enrollment Status Page Profiles"
        CloudPCProvisioningPolicies = "Cloud PC Provisioning"
        CloudPCUserSettings         = "Cloud PC User Settings"
    }

    $totalEffective = 0
    foreach ($key in $categoryDisplay.Keys) {
        $items = $relevantPolicies[$key]
        if ($items.Count -eq 0) { continue }
        $totalEffective += $items.Count
        Write-Host "`n------- $($categoryDisplay[$key]) ($($items.Count)) -------" -ForegroundColor Cyan
        $headerFormat = "{0,-45} {1,-22} {2,-30} {3,-20}" -f "Name", "Source", "Reason", "ID"
        $sep = Get-Separator
        Write-Host $sep
        Write-Host $headerFormat -ForegroundColor Yellow
        Write-Host $sep

        foreach ($item in $items) {
            $name = if (-not [string]::IsNullOrWhiteSpace($item.displayName)) { $item.displayName } else { $item.name }
            if (-not $name) { $name = "Unnamed" }
            if ($name.Length -gt 42) { $name = $name.Substring(0, 39) + "..." }

            $src = if ($item.Source) { $item.Source } else { "-" }
            if ($src.Length -gt 19) { $src = $src.Substring(0, 16) + "..." }

            $reason = if ($item.AssignmentReason) { $item.AssignmentReason } else { "-" }
            if ($reason.Length -gt 27) { $reason = $reason.Substring(0, 24) + "..." }

            $id = if ($item.id) { $item.id } else { "-" }
            if ($id.Length -gt 17) { $id = $id.Substring(0, 14) + "..." }

            $color = if ($src -eq 'Excluded') { 'Red' } else { 'White' }
            Write-Host ("{0,-45} {1,-22} {2,-30} {3,-20}" -f $name, $src, $reason, $id) -ForegroundColor $color
        }
        Write-Host $sep
    }

    Write-Host "`n=== Summary ===" -ForegroundColor Cyan
    if ($totalEffective -eq 0) {
        Write-Host " No effective policies or apps for this user/device combination." -ForegroundColor Yellow
    }
    else {
        Write-Host " $totalEffective effective policy/app assignments found across $((($categoryDisplay.Keys | Where-Object { $relevantPolicies[$_].Count -gt 0 })).Count) categories." -ForegroundColor Green
    }

    # ── Export ───────────────────────────────────────────────────────────────
    $exportData = [System.Collections.ArrayList]::new()
    $null = $exportData.Add([PSCustomObject]@{
        Category         = "What-If Subject"
        Item             = "User: $upn + Device: $($deviceInfo.DisplayName) (ID: $($deviceInfo.Id), OS: $deviceOS)"
        ScopeTags        = ""
        AssignmentReason = "Effective Policy Preview"
    })

    Add-ExportData -ExportData $exportData -Category "Device Configuration"                  -Items $relevantPolicies.DeviceConfigs               -AssignmentReason { param($i) "$($i.Source) | $($i.AssignmentReason)" }
    Add-ExportData -ExportData $exportData -Category "Settings Catalog Policy"               -Items $relevantPolicies.SettingsCatalog             -AssignmentReason { param($i) "$($i.Source) | $($i.AssignmentReason)" }
    Add-ExportData -ExportData $exportData -Category "Compliance Policy"                     -Items $relevantPolicies.CompliancePolicies          -AssignmentReason { param($i) "$($i.Source) | $($i.AssignmentReason)" }
    Add-ExportData -ExportData $exportData -Category "App Protection Policy"                 -Items $relevantPolicies.AppProtectionPolicies       -AssignmentReason { param($i) "$($i.Source) | $($i.AssignmentReason)" }
    Add-ExportData -ExportData $exportData -Category "App Configuration Policy"              -Items $relevantPolicies.AppConfigurationPolicies    -AssignmentReason { param($i) "$($i.Source) | $($i.AssignmentReason)" }
    Add-ExportData -ExportData $exportData -Category "Required App"                          -Items $relevantPolicies.AppsRequired                -AssignmentReason { param($i) "$($i.Source) | $($i.AssignmentReason)" }
    Add-ExportData -ExportData $exportData -Category "Available App"                         -Items $relevantPolicies.AppsAvailable               -AssignmentReason { param($i) "$($i.Source) | $($i.AssignmentReason)" }
    Add-ExportData -ExportData $exportData -Category "Uninstall App"                         -Items $relevantPolicies.AppsUninstall               -AssignmentReason { param($i) "$($i.Source) | $($i.AssignmentReason)" }
    Add-ExportData -ExportData $exportData -Category "Platform Script"                       -Items $relevantPolicies.PlatformScripts             -AssignmentReason { param($i) "$($i.Source) | $($i.AssignmentReason)" }
    Add-ExportData -ExportData $exportData -Category "Proactive Remediation Script"          -Items $relevantPolicies.HealthScripts               -AssignmentReason { param($i) "$($i.Source) | $($i.AssignmentReason)" }
    Add-ExportData -ExportData $exportData -Category "Endpoint Security - Antivirus"         -Items $relevantPolicies.AntivirusProfiles           -AssignmentReason { param($i) "$($i.Source) | $($i.AssignmentReason)" }
    Add-ExportData -ExportData $exportData -Category "Endpoint Security - Disk Encryption"   -Items $relevantPolicies.DiskEncryptionProfiles      -AssignmentReason { param($i) "$($i.Source) | $($i.AssignmentReason)" }
    Add-ExportData -ExportData $exportData -Category "Endpoint Security - Firewall"          -Items $relevantPolicies.FirewallProfiles            -AssignmentReason { param($i) "$($i.Source) | $($i.AssignmentReason)" }
    Add-ExportData -ExportData $exportData -Category "Endpoint Security - EDR"               -Items $relevantPolicies.EndpointDetectionProfiles   -AssignmentReason { param($i) "$($i.Source) | $($i.AssignmentReason)" }
    Add-ExportData -ExportData $exportData -Category "Endpoint Security - ASR"               -Items $relevantPolicies.AttackSurfaceProfiles       -AssignmentReason { param($i) "$($i.Source) | $($i.AssignmentReason)" }
    Add-ExportData -ExportData $exportData -Category "Endpoint Security - Account Protection" -Items $relevantPolicies.AccountProtectionProfiles  -AssignmentReason { param($i) "$($i.Source) | $($i.AssignmentReason)" }
    Add-ExportData -ExportData $exportData -Category "Autopilot Deployment Profile"          -Items $relevantPolicies.DeploymentProfiles          -AssignmentReason { param($i) "$($i.Source) | $($i.AssignmentReason)" }
    Add-ExportData -ExportData $exportData -Category "Enrollment Status Page Profile"        -Items $relevantPolicies.ESPProfiles                 -AssignmentReason { param($i) "$($i.Source) | $($i.AssignmentReason)" }
    Add-ExportData -ExportData $exportData -Category "Cloud PC Provisioning Policy"          -Items $relevantPolicies.CloudPCProvisioningPolicies -AssignmentReason { param($i) "$($i.Source) | $($i.AssignmentReason)" }
    Add-ExportData -ExportData $exportData -Category "Cloud PC User Setting"                 -Items $relevantPolicies.CloudPCUserSettings         -AssignmentReason { param($i) "$($i.Source) | $($i.AssignmentReason)" }

    Export-ResultsIfRequested -ExportData $exportData -DefaultFileName "IntuneUserDeviceAssignments.csv" -ForceExport:$ExportToCSV -CustomExportPath $ExportPath
}