Src/Private/Get-AbrIntuneAppManagement.ps1

function Get-AbrIntuneAppManagement {
    <#
    .SYNOPSIS
    Documents Intune App Management configuration (MAM/MDM apps and protection policies).
    .DESCRIPTION
        Collects and reports on:
          - Mobile Application Management (MAM) App Protection Policies
          - Managed app configurations
          - Published app inventory (top deployed apps)
          - App Assignment summary
    .NOTES
        Version: 0.1.0
        Author: Pai Wei Sing
    #>

    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory)]
        [string]$TenantId
    )

    begin {
        Write-PScriboMessage -Message "Collecting Intune App Management for $TenantId."
        Show-AbrDebugExecutionTime -Start -TitleMessage 'App Management'
    }

    process {
        Section -Style Heading2 'App Management' {
            Paragraph "The following section documents App Management (MAM/MDM) configuration for tenant $TenantId."
            BlankLine

            # Aggregated metrics for compliance checks
            $TotalAppProtPolicies  = 0
            $UnassignedAppPolicies = 0

            #region App Protection Policies
            try {
                Write-Host " - Retrieving App Protection Policies..."

                # iOS App Protection Policies
                $iOSAppProtResp = Invoke-MgGraphRequest -Method GET `
                    -Uri "$($script:GraphEndpoint)/v1.0/deviceAppManagement/iosManagedAppProtections?`$expand=assignments" `
                    -ErrorAction SilentlyContinue
                $iOSPolicies = $iOSAppProtResp.value

                # Android App Protection Policies
                $AndroidAppProtResp = Invoke-MgGraphRequest -Method GET `
                    -Uri "$($script:GraphEndpoint)/v1.0/deviceAppManagement/androidManagedAppProtections?`$expand=assignments" `
                    -ErrorAction SilentlyContinue
                $AndroidPolicies = $AndroidAppProtResp.value

                $AllAppProtPolicies = @()
                if ($iOSPolicies)      { $AllAppProtPolicies += $iOSPolicies      | ForEach-Object { $_ | Add-Member -NotePropertyName '_platform' -NotePropertyValue 'iOS / iPadOS' -PassThru -Force } }
                if ($AndroidPolicies)  { $AllAppProtPolicies += $AndroidPolicies  | ForEach-Object { $_ | Add-Member -NotePropertyName '_platform' -NotePropertyValue 'Android'      -PassThru -Force } }

                $null = ($TotalAppProtPolicies = @($AllAppProtPolicies).Count)

                if ($AllAppProtPolicies -and @($AllAppProtPolicies).Count -gt 0) {
                    Section -Style Heading3 'App Protection Policies' {
                        BlankLine
                        $AppProtObj = [System.Collections.ArrayList]::new()
                        foreach ($AppProt in ($AllAppProtPolicies | Sort-Object displayName)) {
                            $assignResolved = Resolve-IntuneAssignments -Assignments $AppProt.assignments -CheckMemberCount:$script:CheckEmptyGroups
                            $AssignedTo = $assignResolved.AssignmentSummary
                            if ($assignResolved.AssignmentSummary -eq 'Not assigned') { $null = ($UnassignedAppPolicies++) }

                            $appProtInObj = [ordered] @{
                                'Policy Name'             = $AppProt.displayName
                                'Platform'                = $AppProt._platform
                                'Data Transfer Block'     = if ($null -ne $AppProt.allowedOutboundDataTransferDestinations) { $AppProt.allowedOutboundDataTransferDestinations } else { '--' }
                                'PIN Required'            = if ($null -ne $AppProt.pinRequired) { $AppProt.pinRequired } else { '--' }
                                'Managed Browser Required'= if ($null -ne $AppProt.managedBrowserToOpenLinksRequired) { $AppProt.managedBrowserToOpenLinksRequired } else { '--' }
                                'Assignments'             = $AssignedTo
                                'Last Modified'           = if ($AppProt.lastModifiedDateTime) { ([datetime]$AppProt.lastModifiedDateTime).ToString('yyyy-MM-dd') } else { '--' }
                            }
                            $AppProtObj.Add([pscustomobject](ConvertTo-HashToYN $appProtInObj)) | Out-Null
                        }

                        $null = (& {
                            if ($HealthCheck.Intune.AppManagement) {
                                $null = ($AppProtObj | Where-Object { $_.'Assignments' -eq 'Not assigned' } | Set-Style -Style Warning | Out-Null)
                            }
                        })

                        $AppProtTableParams = @{ Name = "App Protection Policies - $TenantId"; ColumnWidths = 22, 14, 16, 11, 14, 13, 10 }
                        if ($Report.ShowTableCaptions) { $AppProtTableParams['Caption'] = "- $($AppProtTableParams.Name)" }
                        $AppProtObj | Table @AppProtTableParams

                        if (Get-IntuneExcelSheetEnabled -SheetKey 'AppProtection') {
                            $script:ExcelSheets['App Protection'] = $AppProtObj
                        }
                        if (Get-IntuneBackupSectionEnabled -SectionKey 'AppProtectionPolicies') { $script:BackupData['AppProtectionPolicies'] = $AllAppProtPolicies }

                        # ── Per-policy detail sections ────────────────────────────────
                        foreach ($AppProt in ($AllAppProtPolicies | Sort-Object displayName)) {
                            $assignResolved = Resolve-IntuneAssignments -Assignments $AppProt.assignments -CheckMemberCount:$script:CheckEmptyGroups

                            Section -Style Heading4 $AppProt.displayName {
                                BlankLine

                                # Overview
                                $APDetailObj = [System.Collections.ArrayList]::new()
                                $APDetailObj.Add([pscustomobject]@{ Setting = 'Policy Name';    Value = $AppProt.displayName }) | Out-Null
                                $APDetailObj.Add([pscustomobject]@{ Setting = 'Platform';       Value = $AppProt._platform }) | Out-Null
                                $APDetailObj.Add([pscustomobject]@{ Setting = 'Created';        Value = if ($AppProt.createdDateTime)      { ([datetime]$AppProt.createdDateTime).ToString('yyyy-MM-dd') }      else { '--' } }) | Out-Null
                                $APDetailObj.Add([pscustomobject]@{ Setting = 'Last Modified';  Value = if ($AppProt.lastModifiedDateTime) { ([datetime]$AppProt.lastModifiedDateTime).ToString('yyyy-MM-dd') } else { '--' } }) | Out-Null
                                $APDetailObj.Add([pscustomobject]@{ Setting = 'Assignments';    Value = $assignResolved.AssignmentSummary }) | Out-Null

                                $APOvTableParams = @{ Name = "Policy Overview - $($AppProt.displayName)"; ColumnWidths = 45, 55 }
                                if ($Report.ShowTableCaptions) { $APOvTableParams['Caption'] = "- $($APOvTableParams.Name)" }
                                $APDetailObj | Table @APOvTableParams
                                BlankLine

                                # ── Data Protection settings ──────────────────────────
                                $DataProtRows = [System.Collections.ArrayList]::new()
                                $DataProtRows.Add([pscustomobject]@{ Setting = 'Data Protection'; Value = '' }) | Out-Null

                                $DataTransferLabel = {
                                    param($v)
                                    switch ($v) {
                                        'allApps'             { 'All apps' }
                                        'managedApps'         { 'Policy managed apps' }
                                        'none'                { 'None' }
                                        default               { if ($v) { $v } else { '--' } }
                                    }
                                }

                                if ($AppProt.allowedOutboundDataTransferDestinations) {
                                    $DataProtRows.Add([pscustomobject]@{ Setting = 'Send org data to other apps'; Value = (& $DataTransferLabel $AppProt.allowedOutboundDataTransferDestinations) }) | Out-Null
                                }
                                if ($AppProt.allowedInboundDataTransferSources) {
                                    $DataProtRows.Add([pscustomobject]@{ Setting = 'Receive data from other apps'; Value = (& $DataTransferLabel $AppProt.allowedInboundDataTransferSources) }) | Out-Null
                                }
                                if ($null -ne $AppProt.saveAsBlocked) {
                                    $DataProtRows.Add([pscustomobject]@{ Setting = 'Save copies of org data'; Value = if ($AppProt.saveAsBlocked) { 'Block' } else { 'Allow' } }) | Out-Null
                                }
                                if ($null -ne $AppProt.copyPasteBlockedToOtherApps) {
                                    $DataProtRows.Add([pscustomobject]@{ Setting = 'Cut and copy between other apps'; Value = if ($AppProt.copyPasteBlockedToOtherApps) { 'Blocked' } else { 'Any app' } }) | Out-Null
                                }
                                if ($null -ne $AppProt.screenCaptureBlocked) {
                                    $DataProtRows.Add([pscustomobject]@{ Setting = 'Screen capture and Google Assistant'; Value = if ($AppProt.screenCaptureBlocked) { 'Block' } else { 'Allow' } }) | Out-Null
                                }
                                if ($AppProt.allowedOutboundClipboardSharingLevel) {
                                    $clipLabel = switch ($AppProt.allowedOutboundClipboardSharingLevel) {
                                        'allApps'      { 'Any app' }
                                        'managedApps'  { 'Policy managed apps' }
                                        'blocked'      { 'Blocked' }
                                        default        { $AppProt.allowedOutboundClipboardSharingLevel }
                                    }
                                    $DataProtRows.Add([pscustomobject]@{ Setting = 'Cut and copy between apps'; Value = $clipLabel }) | Out-Null
                                }
                                if ($null -ne $AppProt.encryptAppData) {
                                    $DataProtRows.Add([pscustomobject]@{ Setting = 'Encrypt org data'; Value = if ($AppProt.encryptAppData) { 'Required' } else { 'Not Required' } }) | Out-Null
                                }
                                if ($null -ne $AppProt.disableAppEncryptionIfDeviceEncryptionIsEnabled) {
                                    $DataProtRows.Add([pscustomobject]@{ Setting = 'Encrypt org data on enrolled devices'; Value = if ($AppProt.disableAppEncryptionIfDeviceEncryptionIsEnabled) { 'Disabled' } else { 'Enabled' } }) | Out-Null
                                }
                                if ($null -ne $AppProt.managedBrowserToOpenLinksRequired) {
                                    $DataProtRows.Add([pscustomobject]@{ Setting = 'Open web content in Managed Browser'; Value = if ($AppProt.managedBrowserToOpenLinksRequired) { 'Required' } else { 'Not Required' } }) | Out-Null
                                }
                                if ($null -ne $AppProt.contactSyncBlocked) {
                                    $DataProtRows.Add([pscustomobject]@{ Setting = 'Sync contacts to native contacts app'; Value = if ($AppProt.contactSyncBlocked) { 'Block' } else { 'Allow' } }) | Out-Null
                                }
                                if ($null -ne $AppProt.printBlocked) {
                                    $DataProtRows.Add([pscustomobject]@{ Setting = 'Printing org data'; Value = if ($AppProt.printBlocked) { 'Block' } else { 'Allow' } }) | Out-Null
                                }
                                if ($AppProt.allowedDataStorageLocations -and @($AppProt.allowedDataStorageLocations).Count -gt 0) {
                                    $DataProtRows.Add([pscustomobject]@{ Setting = 'Allowed data storage locations'; Value = ($AppProt.allowedDataStorageLocations -join ', ') }) | Out-Null
                                }
                                # allowedDataIngestionLocations — paste-source restrictions (HIGH gap fix)
                                if ($AppProt.allowedDataIngestionLocations -and @($AppProt.allowedDataIngestionLocations).Count -gt 0) {
                                    $DataProtRows.Add([pscustomobject]@{ Setting = 'Allowed data ingestion locations'; Value = ($AppProt.allowedDataIngestionLocations -join ', ') }) | Out-Null
                                }
                                # Exempted apps — protocols (iOS) and packages (Android)
                                if ($AppProt.exemptedAppProtocols -and @($AppProt.exemptedAppProtocols).Count -gt 0) {
                                    $exemptStr = ($AppProt.exemptedAppProtocols | ForEach-Object {
                                        $n = if ($_ -is [System.Collections.IDictionary]) { $_['name'] } else { $_.name }
                                        $v = if ($_ -is [System.Collections.IDictionary]) { $_['value'] } else { $_.value }
                                        if ($n) { "$n ($v)" } else { "$v" }
                                    }) -join '; '
                                    $DataProtRows.Add([pscustomobject]@{ Setting = 'Exempt apps (iOS protocols)'; Value = $exemptStr }) | Out-Null
                                }
                                if ($AppProt.exemptedAppPackages -and @($AppProt.exemptedAppPackages).Count -gt 0) {
                                    $exemptPkgStr = ($AppProt.exemptedAppPackages | ForEach-Object {
                                        $n = if ($_ -is [System.Collections.IDictionary]) { $_['name'] } else { $_.name }
                                        $v = if ($_ -is [System.Collections.IDictionary]) { $_['value'] } else { $_.value }
                                        if ($n) { "$n ($v)" } else { "$v" }
                                    }) -join '; '
                                    $DataProtRows.Add([pscustomobject]@{ Setting = 'Exempt apps (Android packages)'; Value = $exemptPkgStr }) | Out-Null
                                }

                                # ── Access Requirements ───────────────────────────────
                                $DataProtRows.Add([pscustomobject]@{ Setting = 'Access Requirements'; Value = '' }) | Out-Null
                                if ($null -ne $AppProt.pinRequired) {
                                    $DataProtRows.Add([pscustomobject]@{ Setting = 'PIN for access'; Value = if ($AppProt.pinRequired) { 'Required' } else { 'Not Required' } }) | Out-Null
                                }
                                if ($AppProt.pinRequiredInsteadOfBiometricTimeout) {
                                    $DataProtRows.Add([pscustomobject]@{ Setting = 'Recheck access requirements after (minutes of inactivity)'; Value = "$($AppProt.pinRequiredInsteadOfBiometricTimeout)" }) | Out-Null
                                }
                                if ($AppProt.maximumPinRetries) {
                                    $DataProtRows.Add([pscustomobject]@{ Setting = 'Max PIN attempts before reset'; Value = "$($AppProt.maximumPinRetries)" }) | Out-Null
                                }
                                if ($null -ne $AppProt.simplePinBlocked) {
                                    $DataProtRows.Add([pscustomobject]@{ Setting = 'Simple PIN'; Value = if ($AppProt.simplePinBlocked) { 'Block' } else { 'Allow' } }) | Out-Null
                                }
                                if ($AppProt.minimumPinLength) {
                                    $DataProtRows.Add([pscustomobject]@{ Setting = 'Minimum PIN length'; Value = "$($AppProt.minimumPinLength)" }) | Out-Null
                                }
                                if ($null -ne $AppProt.fingerprintBlocked) {
                                    $DataProtRows.Add([pscustomobject]@{ Setting = 'Touch ID / Face ID instead of PIN'; Value = if ($AppProt.fingerprintBlocked) { 'Block' } else { 'Allow' } }) | Out-Null
                                }
                                if ($null -ne $AppProt.workAccountBlocked) {
                                    $DataProtRows.Add([pscustomobject]@{ Setting = 'Corporate credentials for access'; Value = if ($AppProt.workAccountBlocked) { 'Block' } else { 'Require' } }) | Out-Null
                                }

                                # ── Conditional Launch ────────────────────────────────
                                $DataProtRows.Add([pscustomobject]@{ Setting = 'Conditional Launch'; Value = '' }) | Out-Null
                                if ($AppProt.periodBeforePinReset) {
                                    $DataProtRows.Add([pscustomobject]@{ Setting = 'Offline grace period (days)'; Value = $AppProt.periodBeforePinReset }) | Out-Null
                                }
                                # periodOnlineBeforeAccessCheck — online recheck interval (MEDIUM gap fix)
                                if ($AppProt.periodOnlineBeforeAccessCheck) {
                                    $DataProtRows.Add([pscustomobject]@{ Setting = 'Online recheck interval'; Value = $AppProt.periodOnlineBeforeAccessCheck }) | Out-Null
                                }
                                if ($AppProt.periodOfflineBeforeWipeInDays -and $AppProt.periodOfflineBeforeWipeInDays -gt 0) {
                                    $DataProtRows.Add([pscustomobject]@{ Setting = 'Offline days before wiping app data'; Value = "$($AppProt.periodOfflineBeforeWipeInDays)" }) | Out-Null
                                }
                                if ($AppProt.minimumOsVersion) {
                                    $DataProtRows.Add([pscustomobject]@{ Setting = 'Minimum OS version'; Value = $AppProt.minimumOsVersion }) | Out-Null
                                }
                                if ($AppProt.minimumAppVersion) {
                                    $DataProtRows.Add([pscustomobject]@{ Setting = 'Minimum app version'; Value = $AppProt.minimumAppVersion }) | Out-Null
                                }
                                if ($AppProt.minimumWarningSdkVersion) {
                                    $DataProtRows.Add([pscustomobject]@{ Setting = 'Minimum warning SDK version'; Value = $AppProt.minimumWarningSdkVersion }) | Out-Null
                                }
                                if ($AppProt.minimumWipeSdkVersion) {
                                    $DataProtRows.Add([pscustomobject]@{ Setting = 'Minimum wipe SDK version'; Value = $AppProt.minimumWipeSdkVersion }) | Out-Null
                                }
                                if ($null -ne $AppProt.jailbrokenDevice -and $AppProt.jailbrokenDevice) {
                                    $DataProtRows.Add([pscustomobject]@{ Setting = 'Jailbroken / rooted devices'; Value = 'Block access' }) | Out-Null
                                }
                                if ($null -ne $AppProt.deviceThreatProtectionEnabled -and $AppProt.deviceThreatProtectionEnabled) {
                                    $threatLabel = if ($AppProt.maximumAllowedDeviceThreatLevel) {
                                        switch ($AppProt.maximumAllowedDeviceThreatLevel) {
                                            'secured'  { 'Secured' }
                                            'low'      { 'Low' }
                                            'medium'   { 'Medium' }
                                            'high'     { 'High' }
                                            default    { $AppProt.maximumAllowedDeviceThreatLevel }
                                        }
                                    } else { 'Secured' }
                                    $DataProtRows.Add([pscustomobject]@{ Setting = 'Device Threat Protection — max allowed level'; Value = $threatLabel }) | Out-Null
                                }

                                # Style section headers bold
                                $secHeaders = @('Data Protection','Access Requirements','Conditional Launch')
                                $null = ($DataProtRows | Where-Object { $_.Setting -in $secHeaders } | Set-Style -Style 'TableSectionHeader')

                                $APSettingsTableParams = @{ Name = "Policy Settings - $($AppProt.displayName)"; ColumnWidths = 55, 45 }
                                if ($Report.ShowTableCaptions) { $APSettingsTableParams['Caption'] = "- $($APSettingsTableParams.Name)" }
                                $DataProtRows | Table @APSettingsTableParams
                            }
                        }
                    }
                } else {
                    Paragraph "No App Protection Policies found in tenant $TenantId."
                }
            } catch {
                    if (Test-AbrGraphForbidden -ErrorRecord $_) {
                        Write-AbrPermissionError -Section 'App Protection Policies' -RequiredRole 'Intune Service Administrator or Global Administrator'
                    } else {
                        Write-AbrSectionError -Section 'App Protection Policies' -Message "$($_.Exception.Message)"
                    }
                }
            #endregion

            #region Published Apps Summary
            if ($InfoLevel.AppManagement -ge 1) {
                try {
                    Write-Host " - Retrieving published apps..."
                    # /beta required -- v1.0 mobileApps returns BadRequest with $select on some tenants
                    # @odata.type is a system property returned automatically -- do not include in $select
                    # Removing $select entirely avoids BadRequest on tenants with strict Graph validation
                    $AppsResp = Invoke-MgGraphRequest -Method GET `
                        -Uri "$($script:GraphEndpoint)/beta/deviceAppManagement/mobileApps" `
                        -ErrorAction SilentlyContinue
                    $Apps = $AppsResp.value
                    # Handle paging for large app libraries
                    while ($AppsResp -and $AppsResp.'@odata.nextLink') {
                        $AppsResp = Invoke-MgGraphRequest -Method GET -Uri $AppsResp.'@odata.nextLink' -ErrorAction SilentlyContinue
                        if ($AppsResp.value) { $Apps += $AppsResp.value }
                    }

                    if ($Apps -and @($Apps).Count -gt 0) {
                        Section -Style Heading3 'Published Apps' {
                            BlankLine

                            # Summary counts by type
                            $AppTypes = $Apps | Group-Object { $_.'@odata.type' -replace '#microsoft.graph.', '' } |
                                Sort-Object Count -Descending
                            $AppSumObj = [System.Collections.ArrayList]::new()
                            foreach ($AT in $AppTypes) {
                                $atObj = [ordered] @{
                                    'App Type'   = $AT.Name
                                    'Count'      = $AT.Count
                                }
                                $AppSumObj.Add([pscustomobject]$atObj) | Out-Null
                            }
                            $AppSumTableParams = @{ Name = "Published App Summary - $TenantId"; ColumnWidths = 65, 35 }
                            if ($Report.ShowTableCaptions) { $AppSumTableParams['Caption'] = "- $($AppSumTableParams.Name)" }
                            $AppSumObj | Table @AppSumTableParams
                            BlankLine

                            # Full app list (InfoLevel 2)
                            if ($InfoLevel.AppManagement -ge 2) {
                                $AppListObj = [System.Collections.ArrayList]::new()
                                foreach ($App in ($Apps | Sort-Object displayName)) {
                                    $AppType = $App.'@odata.type' -replace '#microsoft.graph.', ''
                                    # Resolve per-app assignments (HIGH gap fix)
                                    $appAssignResolved = $null
                                    if ($InfoLevel.AppManagement -ge 2) {
                                        try {
                                            $appAssignResp = $null
                                            try {
                                                $appAssignResp = Invoke-MgGraphRequest -Method GET `
                                                    -Uri "$($script:GraphEndpoint)/beta/deviceAppManagement/mobileApps/$($App.id)/assignments" `
                                                    -ErrorAction Stop
                                            } catch {
                                                Write-AbrDebugLog "App assignments unavailable for '$($App.displayName)': $($_.Exception.Message)" 'WARN' 'AppMgmt'
                                            }
                                            if ($appAssignResp -and $appAssignResp.value) {
                                                $appAssignResolved = Resolve-IntuneAssignments -Assignments $appAssignResp.value -CheckMemberCount:$script:CheckEmptyGroups
                                            }
                                        } catch {
                                            Write-AbrDebugLog "App assignment resolution failed for '$($App.displayName)': $($_.Exception.Message)" 'WARN' 'AppMgmt'
                                        }
                                    }
                                    $publishLabel = switch ($App.publishingState) {
                                        'published'   { 'Published' }
                                        'processing'  { 'Processing' }
                                        'notPublished'{ 'Not Published' }
                                        default       { if ($App.publishingState) { $App.publishingState } else { '--' } }
                                    }
                                    $appInObj = [ordered] @{
                                        'App Name'       = $App.displayName
                                        'Publisher'      = if ($App.publisher) { $App.publisher } else { '--' }
                                        'App Type'       = $AppType
                                        'Version'        = if ($App.version) { $App.version } else { '--' }
                                        'State'          = $publishLabel
                                        'Featured'       = if ($null -ne $App.isFeatured) { if ($App.isFeatured) { 'Yes' } else { 'No' } } else { '--' }
                                        'Assigned To'    = if ($appAssignResolved) { $appAssignResolved.IncludedGroups } else { '--' }
                                        'Created'        = if ($App.createdDateTime) { ([datetime]$App.createdDateTime).ToString('yyyy-MM-dd') } else { '--' }
                                    }
                                    $AppListObj.Add([pscustomobject]$appInObj) | Out-Null
                                }
                                $AppListTableParams = @{ Name = "App Inventory - $TenantId"; ColumnWidths = 22, 16, 16, 8, 9, 7, 14, 8 }
                                if ($Report.ShowTableCaptions) { $AppListTableParams['Caption'] = "- $($AppListTableParams.Name)" }
                                $AppListObj | Table @AppListTableParams

                                if (Get-IntuneExcelSheetEnabled -SheetKey 'AppInventory') {
                                    $script:ExcelSheets['App Inventory'] = $AppListObj
                                }
                            }
                        }
                    }
                } catch {
                        if (Test-AbrGraphForbidden -ErrorRecord $_) {
                            Write-AbrPermissionError -Section 'Published Apps' -RequiredRole 'Intune Service Administrator or Global Administrator'
                        } else {
                            Write-AbrSectionError -Section 'Published Apps' -Message "$($_.Exception.Message)"
                        }
                    }
            }
            #endregion

            #region ACSC E8 Assessment
            if ($script:IncludeACSCe8) {
                BlankLine
                Paragraph "ACSC Essential Eight Maturity Level Assessment -- App Management:"
                BlankLine
                try {
                    $_v = @{
                        TotalAppProtectionPolicies = $TotalAppProtPolicies
                        UnassignedAppPolicies      = $UnassignedAppPolicies
                    }
                    $E8Checks = Build-AbrIntuneComplianceChecks -Definitions (Get-AbrIntuneE8Checks -Section 'AppManagement') -Framework E8 -CallerVariables $_v
                    New-AbrIntuneE8AssessmentTable -Checks $E8Checks -Name 'App Management' -TenantId $TenantId
                    if ($E8Checks) { $null = $script:E8AllChecks.AddRange([object[]](@($E8Checks | Select-Object @{N='Section';E={'AppManagement'}}, ML, Control, Status, Detail))) }
                } catch { Write-AbrSectionError -Section 'E8 App Management Assessment' -Message "$($_.Exception.Message)" }
            }
            #endregion

            #region CIS Assessment
            if ($script:IncludeCISBaseline) {
                BlankLine
                Paragraph "CIS Microsoft 365 Foundations Benchmark Assessment -- App Management:"
                BlankLine
                try {
                    $_v = @{
                        TotalAppProtectionPolicies = $TotalAppProtPolicies
                        UnassignedAppPolicies      = $UnassignedAppPolicies
                    }
                    $CISChecks = Build-AbrIntuneComplianceChecks -Definitions (Get-AbrIntuneCISChecks -Section 'AppManagement') -Framework CIS -CallerVariables $_v
                    New-AbrIntuneCISAssessmentTable -Checks $CISChecks -Name 'App Management' -TenantId $TenantId
                    if ($CISChecks) { $null = $script:CISAllChecks.AddRange([object[]](@($CISChecks | Select-Object @{N='Section';E={'AppManagement'}}, CISControl, Level, Status, Detail))) }
                } catch { Write-AbrSectionError -Section 'CIS App Management Assessment' -Message "$($_.Exception.Message)" }
            }
            #endregion

            #region Windows Information Protection (WIP) Policies — CRITICAL gap fix
            if ($InfoLevel.AppManagement -ge 1) {
                try {
                    Write-Host " - Retrieving Windows Information Protection policies..."
                    $WipMdmResp = $null; $WipMamResp = $null
                    try { $WipMdmResp = Invoke-MgGraphRequest -Method GET -Uri "$($script:GraphEndpoint)/beta/deviceAppManagement/mdmWindowsInformationProtectionPolicies?`$expand=assignments" -ErrorAction Stop } catch { Write-AbrDebugLog "WIP MDM policies unavailable: $($_.Exception.Message)" 'WARN' 'AppMgmt' }
                    try { $WipMamResp = Invoke-MgGraphRequest -Method GET -Uri "$($script:GraphEndpoint)/beta/deviceAppManagement/windowsInformationProtectionPolicies?`$expand=assignments" -ErrorAction Stop } catch { Write-AbrDebugLog "WIP MAM policies unavailable: $($_.Exception.Message)" 'WARN' 'AppMgmt' }
                    $WipPolicies  = @()
                    if ($WipMdmResp  -and $WipMdmResp.value)  { $WipPolicies += $WipMdmResp.value  | ForEach-Object { $_ | Add-Member -NotePropertyName '_wipType' -NotePropertyValue 'MDM' -PassThru -Force } }
                    if ($WipMamResp  -and $WipMamResp.value)  { $WipPolicies += $WipMamResp.value  | ForEach-Object { $_ | Add-Member -NotePropertyName '_wipType' -NotePropertyValue 'MAM' -PassThru -Force } }

                    if ($WipPolicies.Count -gt 0) {
                        Section -Style Heading3 'Windows Information Protection (WIP)' {
                            BlankLine
                            $WipObj = [System.Collections.ArrayList]::new()
                            foreach ($Wip in ($WipPolicies | Sort-Object displayName)) {
                                $assignResolved = Resolve-IntuneAssignments -Assignments $Wip.assignments -CheckMemberCount:$script:CheckEmptyGroups
                                $enforcementLabel = switch ($Wip.enforcementLevel) {
                                    'noProtection' { 'Off' }
                                    'encryptAndAuditOnly' { 'Silent' }
                                    'encryptAuditAndPrompt' { 'Override' }
                                    'encryptAuditAndBlock' { 'Block' }
                                    default { if ($Wip.enforcementLevel) { $Wip.enforcementLevel } else { '--' } }
                                }
                                $protectedAppsStr = if ($Wip.protectedApps -and @($Wip.protectedApps).Count -gt 0) {
                                    $names = $Wip.protectedApps | ForEach-Object {
                                        $n = if ($_ -is [System.Collections.IDictionary]) { $_['displayName'] } else { $_.displayName }
                                        if ($n) { $n } else { 'Unnamed' }
                                    }
                                    "$(@($Wip.protectedApps).Count) app(s): $($names -join '; ')"
                                } else { '--' }
                                $exemptAppsStr = if ($Wip.exemptApps -and @($Wip.exemptApps).Count -gt 0) {
                                    "$(@($Wip.exemptApps).Count) app(s)"
                                } else { '--' }
                                $wipRow = [ordered] @{
                                    'Policy Name'     = $Wip.displayName
                                    'Type'            = $Wip._wipType
                                    'Enforcement'     = $enforcementLabel
                                    'Protected Apps'  = $protectedAppsStr
                                    'Exempt Apps'     = $exemptAppsStr
                                    'Included Groups' = $assignResolved.IncludedGroups
                                    'Excluded Groups' = $assignResolved.ExcludedGroups
                                }
                                $WipObj.Add([pscustomobject]$wipRow) | Out-Null
                            }
                            $WipParams = @{ Name = "WIP Policies - $TenantId"; ColumnWidths = 22, 6, 10, 28, 10, 14, 10 }
                            if ($Report.ShowTableCaptions) { $WipParams['Caption'] = "- $($WipParams.Name)" }
                            $WipObj | Table @WipParams
                        }
                    }
                } catch {
                    if (Test-AbrGraphForbidden -ErrorRecord $_) { Write-AbrPermissionError -Section 'WIP Policies' -RequiredRole 'Intune Service Administrator or Global Administrator' }
                    else { Write-AbrSectionError -Section 'WIP Policies' -Message "$($_.Exception.Message)" }
                }
            }
            #endregion

            #region Apple VPP Tokens — MEDIUM gap fix
            if ($InfoLevel.AppManagement -ge 1) {
                try {
                    Write-Host " - Retrieving Apple VPP tokens..."
                    $VppResp = $null
                    try { $VppResp = Invoke-MgGraphRequest -Method GET -Uri "$($script:GraphEndpoint)/beta/deviceAppManagement/vppTokens" -ErrorAction Stop } catch { Write-AbrDebugLog "VPP tokens unavailable: $($_.Exception.Message)" 'WARN' 'AppMgmt' }
                    if ($VppResp -and $VppResp.value -and @($VppResp.value).Count -gt 0) {
                        Section -Style Heading3 'Apple Volume Purchase Program (VPP) Tokens' {
                            BlankLine
                            $VppObj = [System.Collections.ArrayList]::new()
                            foreach ($Token in ($VppResp.value | Sort-Object organizationName)) {
                                $expiryDate = if ($Token.expirationDateTime) { ([datetime]$Token.expirationDateTime).ToString('yyyy-MM-dd') } else { '--' }
                                $lastSyncDate = if ($Token.lastSyncDateTime) { ([datetime]$Token.lastSyncDateTime).ToString('yyyy-MM-dd') } else { '--' }
                                $stateLabel = switch ($Token.state) {
                                    'valid'   { 'Valid' }
                                    'expired' { 'Expired' }
                                    'invalid' { 'Invalid' }
                                    default   { if ($Token.state) { $Token.state } else { '--' } }
                                }
                                $vppRow = [ordered] @{
                                    'Organisation'   = if ($Token.organizationName) { $Token.organizationName } else { '--' }
                                    'Apple ID'       = if ($Token.appleId) { $Token.appleId } else { '--' }
                                    'State'          = $stateLabel
                                    'Licences'       = if ($null -ne $Token.totalLicenseCount) { "$($Token.usedLicenseCount) / $($Token.totalLicenseCount)" } else { '--' }
                                    'Token Type'     = if ($Token.tokenActionResults) { 'Business' } else { if ($Token.appleId) { 'Education' } else { '--' } }
                                    'Expires'        = $expiryDate
                                    'Last Sync'      = $lastSyncDate
                                }
                                $VppObj.Add([pscustomobject]$vppRow) | Out-Null
                            }
                            $VppParams = @{ Name = "VPP Tokens - $TenantId"; ColumnWidths = 22, 24, 8, 12, 10, 12, 12 }
                            if ($Report.ShowTableCaptions) { $VppParams['Caption'] = "- $($VppParams.Name)" }
                            $VppObj | Table @VppParams
                        }
                    }
                } catch {
                    if (Test-AbrGraphForbidden -ErrorRecord $_) { Write-AbrPermissionError -Section 'Apple VPP Tokens' -RequiredRole 'Intune Service Administrator or Global Administrator' }
                    else { Write-AbrSectionError -Section 'Apple VPP Tokens' -Message "$($_.Exception.Message)" }
                }
            }
            #endregion
        }
    }

    end { Show-AbrDebugExecutionTime -End -TitleMessage 'App Management' }
}