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 } # ── 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 } 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 ($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) { $DataProtRows.Add([pscustomobject]@{ Setting = 'Device Threat Protection'; Value = 'Required' }) | Out-Null } # Style section headers bold $secHeaders = @('Data Protection','Access Requirements','Conditional Launch') $null = ($DataProtRows | Where-Object { $_.Setting -in $secHeaders } | Set-Style -Style Info) $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.', '' $appInObj = [ordered] @{ 'App Name' = $App.displayName 'Publisher' = if ($App.publisher) { $App.publisher } else { '--' } 'App Type' = $AppType 'Created' = if ($App.createdDateTime) { ([datetime]$App.createdDateTime).ToString('yyyy-MM-dd') } else { '--' } 'Last Modified'= if ($App.lastModifiedDateTime) { ([datetime]$App.lastModifiedDateTime).ToString('yyyy-MM-dd') } else { '--' } } $AppListObj.Add([pscustomobject]$appInObj) | Out-Null } $AppListTableParams = @{ Name = "App Inventory - $TenantId"; ColumnWidths = 28, 24, 22, 13, 13 } 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 } } end { Show-AbrDebugExecutionTime -End -TitleMessage 'App Management' } } |