tests/Test-Assessment.51014.ps1
|
<#
.SYNOPSIS App Protection Policies block managed-app access on jailbroken or rooted mobile devices. .DESCRIPTION Checks whether at least one assigned App Protection Policy (MAM) per in-scope mobile platform enforces a hard block or wipe on jailbroken iOS / iPadOS and rooted Android devices. Without this gate, the OS sandbox is broken and every other MAM control (cut/copy/paste restrictions, encryption, conditional launch) can be bypassed silently, creating a clear exfiltration path for corporate data. iOS and Android are evaluated independently: iOS uses deviceComplianceRequired + appActionIfDeviceComplianceRequired; Android uses requiredAndroidSafetyNetDeviceAttestationType + appActionIfAndroidSafetyNetDeviceAttestationFailed. .NOTES Test ID: 51014 Category: Devices Pillar: Devices Required API: Microsoft Graph beta — deviceManagement/managedDevices (Q1, count only) deviceAppManagement/iosManagedAppProtections (Q2) deviceAppManagement/androidManagedAppProtections (Q3) #> function Test-Assessment-51014 { [ZtTest( Category = 'Devices', CompatibleLicense = ('INTUNE_A'), ImplementationCost = 'Low', Pillar = 'Devices', RiskLevel = 'High', Service = ('Graph'), SfiPillar = 'Protect identities and secrets', TenantType = ('Workforce'), TestId = 51014, Title = 'App Protection Policies block managed-app access on jailbroken or rooted mobile devices', UserImpact = 'Low' )] [CmdletBinding()] param() #region Data Collection Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose $activity = 'Checking App Protection Policies for jailbreak and root enforcement' # Q1: Count enrolled iOS / iPadOS devices Write-ZtProgress -Activity $activity -Status 'Counting enrolled iOS / iPadOS devices' $iosInvestigateReason = $null $androidInvestigateReason = $null $iosDeviceCount = 0 $androidDeviceCount = 0 try { $iosResult = Invoke-ZtGraphRequest -RelativeUri 'deviceManagement/managedDevices' -Filter "operatingSystem eq 'iOS' or operatingSystem eq 'iPadOS'" -Select 'id' -Top 1 -QueryParameters @{'$count' = 'true'} -ApiVersion beta -DisablePaging -ErrorAction Stop $iosDeviceCount = $iosResult.'@odata.count' } catch { Write-PSFMessage "Failed to retrieve iOS/iPadOS device count: $_" -Level Warning $iosInvestigateReason = 'Unable to retrieve enrolled iOS / iPadOS device count. Verify DeviceManagementManagedDevices.Read.All is granted and retry.' } # Q1 (continued): Count enrolled Android devices Write-ZtProgress -Activity $activity -Status 'Counting enrolled Android devices' try { $androidResult = Invoke-ZtGraphRequest -RelativeUri 'deviceManagement/managedDevices' -Filter "operatingSystem eq 'Android'" -Select 'id' -Top 1 -QueryParameters @{'$count' = 'true'} -ApiVersion beta -DisablePaging -ErrorAction Stop $androidDeviceCount = $androidResult.'@odata.count' } catch { Write-PSFMessage "Failed to retrieve Android device count: $_" -Level Warning $androidInvestigateReason = 'Unable to retrieve enrolled Android device count. Verify DeviceManagementManagedDevices.Read.All is granted and retry.' } # If both Q1 queries failed we cannot determine scope for either platform — early-return Investigate. if ($iosInvestigateReason -and $androidInvestigateReason) { $params = @{ TestId = '51014' Title = 'App Protection Policies block managed-app access on jailbroken or rooted mobile devices' Status = $false Result = '⚠️ Unable to retrieve enrolled device counts for iOS / iPadOS and Android. The API returned an authorization (401/403) or transient (5xx) error, so coverage could not be determined. Re-run after verifying caller permissions — Global Reader at tenant scope.' CustomStatus = 'Investigate' } Add-ZtTestResultDetail @params return } # A platform is in scope only when its Q1 succeeded and returned at least one enrolled device. $iosInScope = ($null -eq $iosInvestigateReason) -and ($iosDeviceCount -gt 0) $androidInScope = ($null -eq $androidInvestigateReason) -and ($androidDeviceCount -gt 0) # If both Q1s succeeded but neither platform has enrolled devices, the check is not applicable. if ((-not $iosInScope) -and (-not $androidInScope) -and (-not $iosInvestigateReason) -and (-not $androidInvestigateReason)) { Add-ZtTestResultDetail -SkippedBecause NotApplicable -Result 'No iOS / iPadOS or Android devices are enrolled in this tenant; the check is not in scope.' return } # Q2: Retrieve all iOS App Protection Policies (only when iOS is in scope). $iosPolicies = @() if ($iosInScope) { Write-ZtProgress -Activity $activity -Status 'Retrieving iOS App Protection Policies' try { $iosPolicies = @(Invoke-ZtGraphRequest -RelativeUri 'deviceAppManagement/iosManagedAppProtections' -Select 'id,displayName,deviceComplianceRequired,appActionIfDeviceComplianceRequired,isAssigned' -ApiVersion beta -ErrorAction Stop) } catch { Write-PSFMessage "Failed to retrieve iOS App Protection Policies: $_" -Level Warning $iosInvestigateReason = 'Unable to retrieve iOS App Protection Policies. Verify DeviceManagementApps.Read.All is granted and retry.' } } # Q3: Retrieve all Android App Protection Policies (only when Android is in scope). $androidPolicies = @() if ($androidInScope) { Write-ZtProgress -Activity $activity -Status 'Retrieving Android App Protection Policies' try { $androidPolicies = @(Invoke-ZtGraphRequest -RelativeUri 'deviceAppManagement/androidManagedAppProtections' -Select 'id,displayName,requiredAndroidSafetyNetDeviceAttestationType,appActionIfAndroidSafetyNetDeviceAttestationFailed,isAssigned' -ApiVersion beta -ErrorAction Stop) } catch { Write-PSFMessage "Failed to retrieve Android App Protection Policies: $_" -Level Warning $androidInvestigateReason = 'Unable to retrieve Android App Protection Policies. Verify DeviceManagementApps.Read.All is granted and retry.' } } # If both platforms have a reason set (from Q1 or Q2/Q3 failures) neither can be evaluated — early-return Investigate. if ($iosInvestigateReason -and $androidInvestigateReason) { $params = @{ TestId = '51014' Title = 'App Protection Policies block managed-app access on jailbroken or rooted mobile devices' Status = $false Result = '⚠️ The Intune App Protection Policies API returned an authorization (401/403) or transient (5xx) error, so coverage could not be determined. Re-run after verifying caller permissions — Global Reader at tenant scope.' CustomStatus = 'Investigate' } Add-ZtTestResultDetail @params return } #endregion Data Collection #region Assessment Logic $acceptableActions = @('block', 'wipe') $acceptableAttestationTypes = @('basicIntegrity', 'basicIntegrityAndDeviceCertification') $iosDeepLinkTemplate = 'https://intune.microsoft.com/#view/Microsoft_Intune/PolicyInstanceMenuBlade/~/0/policyId/{0}/policyOdataType/%23microsoft.graph.iosManagedAppProtection/policyName/{1}' $androidDeepLinkTemplate = 'https://intune.microsoft.com/#view/Microsoft_Intune/PolicyInstanceMenuBlade/~/0/policyId/{0}/policyOdataType/%23microsoft.graph.androidManagedAppProtection/policyName/{1}' # Annotate each policy with Status and PolicyDeepLink once — used by both pass/fail logic and table rendering foreach ($policy in $iosPolicies) { $passes = $policy.deviceComplianceRequired -eq $true -and $policy.appActionIfDeviceComplianceRequired -in $acceptableActions -and $policy.isAssigned -eq $true $encodedName = [System.Uri]::EscapeDataString($policy.displayName) $policy | Add-Member -MemberType NoteProperty -Name Status -Value $passes -Force $policy | Add-Member -MemberType NoteProperty -Name PolicyDeepLink -Value ($iosDeepLinkTemplate -f $policy.id, $encodedName) -Force } foreach ($policy in $androidPolicies) { # Android root detection uses SafetyNet / Play Integrity attestation properties, not deviceComplianceRequired. # requiredAndroidSafetyNetDeviceAttestationType must be non-none, otherwise no attestation runs # and the appActionIf...Failed setting never fires — rooted devices silently pass through. $passes = $policy.requiredAndroidSafetyNetDeviceAttestationType -in $acceptableAttestationTypes -and $policy.appActionIfAndroidSafetyNetDeviceAttestationFailed -in $acceptableActions -and $policy.isAssigned -eq $true $encodedName = [System.Uri]::EscapeDataString($policy.displayName) $policy | Add-Member -MemberType NoteProperty -Name Status -Value $passes -Force $policy | Add-Member -MemberType NoteProperty -Name PolicyDeepLink -Value ($androidDeepLinkTemplate -f $policy.id, $encodedName) -Force } $iosPassingPolicies = @($iosPolicies | Where-Object Status) $androidPassingPolicies = @($androidPolicies | Where-Object Status) # Per-platform verdict — priority order: Investigate / Skipped / Pass / Fail $iosVerdict = if ($iosInvestigateReason) { 'Investigate' } elseif (-not $iosInScope) { 'Skipped' } elseif ($iosPassingPolicies.Count -gt 0) { 'Pass' } else { 'Fail' } $androidVerdict = if ($androidInvestigateReason) { 'Investigate' } elseif (-not $androidInScope) { 'Skipped' } elseif ($androidPassingPolicies.Count -gt 0) { 'Pass' } else { 'Fail' } # Overall verdict — priority: Fail > Investigate > Pass $overallVerdict = if ($iosVerdict -eq 'Fail' -or $androidVerdict -eq 'Fail') { 'Fail' } elseif ($iosVerdict -eq 'Investigate' -or $androidVerdict -eq 'Investigate') { 'Investigate' } else { 'Pass' } $passed = $overallVerdict -eq 'Pass' $customStatus = $null if ($overallVerdict -eq 'Investigate') { $customStatus = 'Investigate' $testResultMarkdown = "The Intune App Protection Policies API returned an authorization (401/403) or transient (5xx) error, so coverage could not be determined. Re-run after verifying caller permissions — Global Reader at tenant scope.`n`n%TestResult%" } elseif ($passed) { $testResultMarkdown = "✅ For every in-scope mobile platform with enrolled devices, an assigned App Protection Policy blocks or wipes managed-app access on jailbroken / rooted devices.`n`n%TestResult%" } else { $testResultMarkdown = "❌ One or more in-scope mobile platforms (iOS / iPadOS, Android) has no assigned App Protection Policy that blocks or wipes managed-app access on jailbroken / rooted devices — the MAM data-protection model can be silently bypassed by users on compromised personal devices.`n`n%TestResult%" } #endregion Assessment Logic #region Report Generation $portalLink = 'https://intune.microsoft.com/#view/Microsoft_Intune_DeviceSettings/AppsMenu/~/protection' $maxRows = 10 # Overall verdict line $overallMd = "**Overall: $overallVerdict**`n`n" # iOS / iPadOS section if ($iosVerdict -eq 'Investigate') { $iosTableMd = @" ### [iOS / iPadOS App Protection Policies — Jailbreak Gate]($portalLink) **Status: Investigate** — $iosInvestigateReason "@ } elseif (-not $iosInScope) { $iosTableMd = @" ### [iOS / iPadOS App Protection Policies — Jailbreak Gate]($portalLink) **Status: Skipped** — No iOS / iPadOS devices enrolled in this tenant. "@ } else { $iosTotalCount = @($iosPolicies).Count $iosQualifyCount = @($iosPassingPolicies).Count $iosDeviceLabel = if ($iosDeviceCount -eq 1) { 'device' } else { 'devices' } $iosPolicyLabel = if ($iosTotalCount -eq 1) { 'policy qualifies' } else { 'policies qualify' } $iosHeaderLine = "**Status: $iosVerdict** — $iosDeviceCount iOS / iPadOS $iosDeviceLabel enrolled; $iosQualifyCount of $iosTotalCount $iosPolicyLabel." if ($iosTotalCount -eq 0) { $iosTableMd = @" ### [iOS / iPadOS App Protection Policies — Jailbreak Gate]($portalLink) $iosHeaderLine No App Protection Policies found for iOS / iPadOS. "@ } else { $iosRows = '' foreach ($policy in ($iosPolicies | Sort-Object @{ Expression = 'Status'; Descending = $true }, displayName | Select-Object -First $maxRows)) { $policyNameSafe = Get-SafeMarkdown $policy.displayName $complianceCell = if ($policy.deviceComplianceRequired) { '✅ True' } else { '❌ False' } $actionCell = if ($policy.appActionIfDeviceComplianceRequired -in $acceptableActions) { "✅ $($policy.appActionIfDeviceComplianceRequired)" } else { "❌ $($policy.appActionIfDeviceComplianceRequired)" } $assignedCell = if ($policy.isAssigned) { '✅ Yes' } else { '❌ No' } $statusCell = if ($policy.Status) { '✅ Pass' } else { '❌ Fail' } $iosRows += "| [$policyNameSafe]($($policy.PolicyDeepLink)) | $complianceCell | $actionCell | $assignedCell | $statusCell |`n" } if ($iosTotalCount -gt $maxRows) { $iosRows += "| ... | | | | _$iosTotalCount total_ |`n" } $iosTableMd = @" ### [iOS / iPadOS App Protection Policies — Jailbreak Gate]($portalLink) $iosHeaderLine | Policy name | Device compliance required | Action on Non-Compliance | Assigned | Status | | :---------- | :------------------------- | :----------------------- | :------- | :----- | $iosRows "@ } } # Android section if ($androidVerdict -eq 'Investigate') { $androidTableMd = @" ### [Android App Protection Policies — Root Gate]($portalLink) **Status: Investigate** — $androidInvestigateReason "@ } elseif (-not $androidInScope) { $androidTableMd = @" ### [Android App Protection Policies — Root Gate]($portalLink) **Status: Skipped** — No Android devices enrolled in this tenant. "@ } else { $androidTotalCount = @($androidPolicies).Count $androidQualifyCount = @($androidPassingPolicies).Count $androidDeviceLabel = if ($androidDeviceCount -eq 1) { 'device' } else { 'devices' } $androidPolicyLabel = if ($androidTotalCount -eq 1) { 'policy qualifies' } else { 'policies qualify' } $androidHeaderLine = "**Status: $androidVerdict** — $androidDeviceCount Android $androidDeviceLabel enrolled; $androidQualifyCount of $androidTotalCount $androidPolicyLabel." if ($androidTotalCount -eq 0) { $androidTableMd = @" ### [Android App Protection Policies — Root Gate]($portalLink) $androidHeaderLine No App Protection Policies found for Android. "@ } else { $androidRows = '' foreach ($policy in ($androidPolicies | Sort-Object @{ Expression = 'Status'; Descending = $true }, displayName | Select-Object -First $maxRows)) { $policyNameSafe = Get-SafeMarkdown $policy.displayName $attestationCell = if ($policy.requiredAndroidSafetyNetDeviceAttestationType -in $acceptableAttestationTypes) { "✅ $($policy.requiredAndroidSafetyNetDeviceAttestationType)" } else { "❌ $($policy.requiredAndroidSafetyNetDeviceAttestationType)" } $actionCell = if ($policy.appActionIfAndroidSafetyNetDeviceAttestationFailed -in $acceptableActions) { "✅ $($policy.appActionIfAndroidSafetyNetDeviceAttestationFailed)" } else { "❌ $($policy.appActionIfAndroidSafetyNetDeviceAttestationFailed)" } $assignedCell = if ($policy.isAssigned) { '✅ Yes' } else { '❌ No' } $statusCell = if ($policy.Status) { '✅ Pass' } else { '❌ Fail' } $androidRows += "| [$policyNameSafe]($($policy.PolicyDeepLink)) | $attestationCell | $actionCell | $assignedCell | $statusCell |`n" } if ($androidTotalCount -gt $maxRows) { $androidRows += "| ... | | | | _$androidTotalCount total_ |`n" } $androidTableMd = @" ### [Android App Protection Policies — Root Gate]($portalLink) $androidHeaderLine | Policy name | Attestation type requested | Action on attestation failure | Assigned | Status | | :---------- | :------------------------- | :---------------------------- | :------- | :----- | $androidRows "@ } } $mdInfo = '{0}{1}{2}' -f $overallMd, $iosTableMd, $androidTableMd $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo #endregion Report Generation $params = @{ TestId = '51014' Title = 'App Protection Policies block managed-app access on jailbroken or rooted mobile devices' Status = $passed Result = $testResultMarkdown } if ($customStatus) { $params.CustomStatus = $customStatus } Add-ZtTestResultDetail @params } |