Public/Authentication/Test-IntunePrerequisites.ps1

function Test-IntunePrerequisites {
    <#
    .SYNOPSIS
        Validates Intune tenant prerequisites
    .DESCRIPTION
        Checks for Intune license availability, Azure AD Premium P2 license (for risk-based
        Conditional Access), and required Microsoft Graph permission scopes.
 
        Non-blocking notes are emitted when Premium P2 is not found, as certain Conditional
        Access policies that use sign-in risk or user risk conditions require this license level.
    .PARAMETER RequiredScopes
        Delegated Graph scopes requested during interactive authentication. When omitted,
        scopes are resolved from the selected imports for direct prerequisite checks.
    .EXAMPLE
        Test-IntunePrerequisites
    #>

    [CmdletBinding()]
    param(
        [Parameter()]
        [hashtable]$Imports = @{},

        [Parameter()]
        [hashtable]$MobileAppConfiguration = @{},

        [Parameter()]
        [string[]]$MobileAppPlatforms = @('All'),

        [Parameter()]
        [string[]]$AppProtectionPlatforms = @('All'),

        [Parameter()]
        [string[]]$BaselinePlatforms = @('All'),

        [Parameter()]
        [string[]]$RequiredScopes
    )

    Write-Information (Format-HydrationDisplayMessage -Message 'Validating Intune prerequisites...' -Style 'Section' -Emoji '🔎') -InformationAction Continue

    $issues = @()
    $notes = [System.Collections.Generic.List[object]]::new()
    $riskBasedPolicyNames = @()
    $conditionalAccessSelected = $Imports.Count -eq 0 -or ($Imports.ContainsKey('conditionalAccess') -and $Imports.conditionalAccess)

    $requiredScopes = if ($RequiredScopes) {
        $RequiredScopes
    } else {
        Get-HydrationGraphScopes `
            -Imports $Imports `
            -MobileAppConfiguration $MobileAppConfiguration `
            -MobileAppPlatforms $MobileAppPlatforms
    }

    try {
        # Check organization info and licenses
        $org = Invoke-MgGraphRequest -Method GET -Uri "beta/organization?`$select=id,displayName" -ErrorAction Stop
        $orgDetails = $org.value[0]

        Write-Information (Format-HydrationDisplayMessage -Message "Connected to: $($orgDetails.displayName)" -Style 'Info' -Emoji '🏢') -InformationAction Continue

        # Check for Intune service plan
        $subscribedSkus = Invoke-MgGraphRequest -Method GET -Uri "beta/subscribedSkus?`$select=id,skuPartNumber,capabilityStatus,servicePlans" -ErrorAction Stop
        $intuneServicePlans = @(
            'INTUNE_A',           # Intune Plan 1
            'INTUNE_EDU',         # Intune for Education
            'INTUNE_SMBIZ',       # Intune Small Business
            'AAD_PREMIUM',        # Azure AD Premium (includes some Intune features)
            'EMSPREMIUM'          # Enterprise Mobility + Security
        )

        # Premium P2 service plans (required for risk-based Conditional Access)
        $premiumP2ServicePlans = Get-PremiumP2ServicePlans

        $hasIntune = $false
        $hasPremiumP2 = $false

        foreach ($sku in $subscribedSkus.value) {
            # Skip disabled SKUs
            if ($sku.capabilityStatus -ne 'Enabled') {
                continue
            }

            foreach ($plan in $sku.servicePlans) {
                if ($plan.servicePlanName -in $intuneServicePlans -and $plan.provisioningStatus -eq 'Success') {
                    $hasIntune = $true
                    Write-Verbose "Found Intune license: $($plan.servicePlanName)"
                }
                if ($plan.servicePlanName -in $premiumP2ServicePlans -and $plan.provisioningStatus -eq 'Success') {
                    $hasPremiumP2 = $true
                    Write-Verbose "Found Premium P2 compatible license: $($plan.servicePlanName) in SKU $($sku.skuPartNumber)"
                }
            }
        }

        if (-not $hasIntune) {
            $issues += "No active Intune license found. Please ensure Intune is licensed for this tenant."
        }

        if ($conditionalAccessSelected -and -not $hasPremiumP2) {
            $riskBasedPolicyNames = @(
                'Require multifactor authentication for risky sign-ins'
                'Require password change for high-risk users'
                'Block high risk agent identities'
                'Block access to Office365 apps for users with insider risk'
            )
            $notes.Add([PSCustomObject]@{
                    Message     = 'Azure AD Premium P2 not detected. Risk-based Conditional Access templates will be skipped:'
                    DetailLines = $riskBasedPolicyNames
                })
        }

        if ($conditionalAccessSelected) {
            $notes.Add([PSCustomObject]@{
                    Message     = 'Some Conditional Access templates use private preview features and will be skipped unless the tenant is explicitly authorized.'
                    DetailLines = @()
                })
        }

        # Check for required permission scopes
        $context = Get-MgContext
        if ($null -eq $context) {
            $issues += "Not connected to Microsoft Graph. Please run Connect-IntuneHydration first."
        } else {
            $isAppOnly = $context.AuthType -eq 'AppOnly' -or ($context.ClientId -and -not $context.Account)
            if ($isAppOnly) {
                # App-only auth uses app roles, so delegated scope validation does not apply
                $notes.Add([PSCustomObject]@{
                        Message     = 'App-only authentication detected; delegated scope validation was skipped.'
                        DetailLines = @()
                    })
            } else {
                $currentScopes = $context.Scopes
                $missingScopes = @($requiredScopes | Where-Object { $currentScopes -notcontains $_ })

                if ($missingScopes.Count -gt 0) {
                    $issues += "Missing required permission scopes: $($missingScopes -join ', ')"
                } else {
                    Write-Verbose 'All required permission scopes are present'
                }
            }
        }

        if ($issues.Count -eq 0) {
            $workloadAccessIssues = @(Test-HydrationGraphWorkloadAccess `
                    -Imports $Imports `
                    -MobileAppConfiguration $MobileAppConfiguration `
                    -MobileAppPlatforms $MobileAppPlatforms `
                    -AppProtectionPlatforms $AppProtectionPlatforms `
                    -BaselinePlatforms $BaselinePlatforms)
            if ($workloadAccessIssues.Count -gt 0) {
                $issues += $workloadAccessIssues
            }
        }

        if ($notes.Count -gt 0) {
            Write-Information (Format-HydrationDisplayMessage -Message 'Notes:' -Style 'Section' -Emoji '📝') -InformationAction Continue
            foreach ($note in $notes) {
                Write-Information (Format-HydrationDisplayMessage -Message $note.Message -Style 'Info' -Emoji '•' -Indent 2) -InformationAction Continue

                foreach ($policyName in $note.DetailLines) {
                    Write-Information (Format-HydrationDisplayMessage -Message "'$policyName'" -Style 'Muted' -Emoji '↳' -Indent 4) -InformationAction Continue
                }
            }
        }

        # Report results
        if ($issues.Count -gt 0) {
            foreach ($issue in $issues) {
                Write-Warning $issue
            }

            # Surface specific issues in the exception message so callers/tests can pattern match
            $issueMessage = $issues -join ' | '
            $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                [System.Exception]::new("Prerequisite checks failed: $issueMessage"),
                'PrerequisiteCheckFailed',
                [System.Management.Automation.ErrorCategory]::NotEnabled,
                $null
            )
            $PSCmdlet.ThrowTerminatingError($errorRecord)
        }

        Write-Information (Format-HydrationDisplayMessage -Message 'Pre-flight checks passed' -Style 'Success' -Emoji '✅') -InformationAction Continue
        return $true
    } catch {
        if ($_.Exception.Message -match "Prerequisite checks failed") {
            throw
        }
        $errorRecord = [System.Management.Automation.ErrorRecord]::new(
            [System.Exception]::new("Failed to validate prerequisites: $($_.Exception.Message)", $_.Exception),
            'PrerequisiteValidationFailed',
            [System.Management.Automation.ErrorCategory]::NotSpecified,
            $null
        )
        $PSCmdlet.ThrowTerminatingError($errorRecord)
    }
}