Private/Initialize-EntraService.ps1

function Initialize-EntraService {
    <#
    .SYNOPSIS
        Initializes the Entra ID service by pre-loading all required resources.

    .DESCRIPTION
        Loads Entra ID (Azure AD) resources from Microsoft Graph API and caches them
        in $script:EntraService for use by check scripts. This follows the singleton
        service pattern to avoid redundant API calls.

        Resources loaded:
        - Users and their MFA registration status
        - Directory roles and role members
        - Security defaults policy
        - Authorization policy settings
        - Conditional access policies
        - Named locations
        - Group settings

    .EXAMPLE
        Initialize-EntraService
        $script:EntraService.Users # Access cached users
    #>

    [CmdletBinding()]
    [OutputType([void])]
    param()

    $ErrorActionPreference = 'Stop'

    $graphApiBase = $script:Config.azure.endpoints.graphApi

    # Initialize service hashtable
    $script:EntraService = @{
        Users                     = $null
        UserMFAStatus             = $null
        DirectoryRoles            = $null
        DirectoryRoleMembers      = @{}
        SecurityDefaults          = $null
        AuthorizationPolicy       = $null
        ConditionalAccessPolicies = $null
        NamedLocations            = $null
        GroupSettings             = $null
    }

    # Load paginated resources
    Write-CIEMLog -Severity DEBUG -Message "Loading users..."
    $usersUri = "$graphApiBase/users?`$select=id,displayName,userPrincipalName,accountEnabled,userType"
    $script:EntraService.Users = @(Get-AllGraphPage -Uri $usersUri -ResourceName "Users")

    # Load user MFA status - requires Azure AD Premium P1/P2 license
    # Handle gracefully if tenant doesn't have the required license
    Write-CIEMLog -Severity DEBUG -Message "Loading user MFA status..."
    $mfaUri = "$graphApiBase/reports/authenticationMethods/userRegistrationDetails"
    try {
        $script:EntraService.UserMFAStatus = @(Get-AllGraphPage -Uri $mfaUri -ResourceName "UserMFAStatus")
    }
    catch {
        # Check for common licensing errors
        if ($_.Exception.Message -match 'RequestFromNonPremiumTenantOrB2CTenant|premium license|403') {
            Write-CIEMLog -Severity WARNING -Message "MFA status data unavailable - Azure AD Premium license required. MFA-related checks will be skipped."
            $script:EntraService.UserMFAStatus = $null
            $script:EntraService.MFAStatusUnavailable = $true
        }
        else {
            # Re-throw other errors
            throw
        }
    }

    # Define non-paginated API endpoints to load - data-driven pattern
    # Some endpoints require Azure AD Premium (ConditionalAccessPolicies, NamedLocations)
    $apiEndpoints = @{
        DirectoryRoles      = @{ Path = 'directoryRoles'; RequiresPremium = $false }
        SecurityDefaults    = @{ Path = 'policies/identitySecurityDefaultsEnforcementPolicy'; RequiresPremium = $false }
        AuthorizationPolicy = @{ Path = 'policies/authorizationPolicy'; RequiresPremium = $false }
        GroupSettings       = @{ Path = 'groupSettings'; RequiresPremium = $false }
        # Premium-required endpoints
        ConditionalAccessPolicies = @{ Path = 'identity/conditionalAccess/policies'; RequiresPremium = $true }
        NamedLocations            = @{ Path = 'identity/conditionalAccess/namedLocations'; RequiresPremium = $true }
    }

    foreach ($endpoint in $apiEndpoints.GetEnumerator()) {
        $params = @{
            Uri          = "$graphApiBase/$($endpoint.Value.Path)"
            ResourceName = $endpoint.Key
        }
        try {
            $script:EntraService[$endpoint.Key] = Invoke-AzureApi @params
        }
        catch {
            if ($endpoint.Value.RequiresPremium -and ($_.Exception.Message -match 'RequestFromNonPremiumTenantOrB2CTenant|premium license|403')) {
                Write-CIEMLog -Severity WARNING -Message "$($endpoint.Key) data unavailable - Azure AD Premium license required."
                $script:EntraService[$endpoint.Key] = $null
            }
            else {
                # Re-throw non-premium errors
                throw
            }
        }
    }

    # Load members for each directory role
    if ($script:EntraService.DirectoryRoles) {
        foreach ($role in $script:EntraService.DirectoryRoles) {
            $params = @{
                Uri          = "$graphApiBase/directoryRoles/$($role.id)/members"
                ResourceName = "DirectoryRole Members ($($role.displayName))"
            }
            $script:EntraService.DirectoryRoleMembers[$role.id] = Invoke-AzureApi @params
        }
    }

    # Log summary
    $counts = @{
        Users    = if ($script:EntraService.Users) { $script:EntraService.Users.Count } else { 0 }
        Roles    = if ($script:EntraService.DirectoryRoles) { $script:EntraService.DirectoryRoles.Count } else { 0 }
        Policies = if ($script:EntraService.ConditionalAccessPolicies) { $script:EntraService.ConditionalAccessPolicies.Count } else { 0 }
        MFAData  = if ($script:EntraService.UserMFAStatus) { $script:EntraService.UserMFAStatus.Count } else { 'N/A (Premium required)' }
    }

    Write-CIEMLog -Severity DEBUG -Message "Entra service initialized: $($counts.Users) users, $($counts.Roles) roles, $($counts.Policies) CA policies, MFA data: $($counts.MFAData)"
}