Public/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.
    .EXAMPLE
        Test-IntunePrerequisites
    #>

    [CmdletBinding()]
    param()

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

    $issues = @()
    $notes = [System.Collections.Generic.List[object]]::new()
    $riskBasedPolicyNames = @()

    # Required scopes from Connect-IntuneHydration
    $requiredScopes = Get-HydrationGraphScopes

    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 (-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
                })
        }

        $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 ($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)
    }
}