Public/Core/Connect-EntraOps.ps1

<#
.SYNOPSIS
    Establishes connections to required PowerShell modules and requests EntraOps access tokens.
 
.DESCRIPTION
    Connection to Azure Resource Management and Microsoft Graph API by using Connect-AzAccount and Connect-MgGraph.
 
.PARAMETER AuthenticationType
    Type of authentication to be used for Azure and Microsoft Graph. Default is "AlreadyAuthenticated".
 
.PARAMETER UseInvokeRestMethodOnly
    Use Invoke-RestMethod instead of the Microsoft Graph SDK (Invoke-MgGraphRequest) for all
    Invoke-EntraOps*Query cmdlets in this session. The Microsoft Graph SDK is neither required nor
    installed in this mode; tokens are provided via -MsGraphAccessToken or acquired with
    Get-AzAccessToken from the Az PowerShell context. Can also be set through the config file
    (top-level setting "UseInvokeRestMethodOnly"); an explicit parameter wins over the config file.
    Note: parallel object resolution requires the Graph SDK and falls back to sequential processing
    in this mode. Connect-EntraOps itself also skips Connect-MgGraph for service-principal/managed-
    identity AuthenticationType values (SystemAssignedMSI, UserAssignedMSI, FederatedCredentials,
    AlreadyAuthenticated) in this mode - their Graph tokens come from Get-AzAccessToken instead, since
    application permissions are fully contained in a client-credentials token without needing
    per-request scopes. UserInteractive and DeviceAuthentication still call Connect-MgGraph (and thus
    still require Microsoft.Graph.Authentication) even with this switch enabled, because only
    Connect-MgGraph -Scopes can trigger the interactive/incremental consent prompt that delegated
    Graph scopes require; a warning is emitted when this fallback applies.
 
.EXAMPLE
    Using Interactive Sign-In of User with double authentication to Az PowerShell (Connect-AzAccount) and Microsoft Graph SDK (Connect-MgGraph)
    Connect-EntraOps -AuthenticationType "UserInteractive" -TenantName "contoso.onmicrosoft.com"
 
.EXAMPLE
    Using authenticated session to Az PowerShell (Connect-AzAccount) in GitHub workflow or any other workload identity environment to request access token for Microsoft Graph SDK (by Get-AzAccessToken) without any further initial authentication.
    Connect-EntraOps -AuthenticationType "AlreadyAuthenticated" -TenantName "contoso.onmicrosoft.com"
 
.EXAMPLE
    Using Managed Identity (User Assigned) to sign-in to Azure and Microsoft Graph
    Connect-EntraOps -AuthenticationType "UserAssignedMSI" -AccountId "00000000-0000-0000-0000-000000000000" -TenantName "contoso.onmicrosoft.com"
#>

function Connect-EntraOps {
    # -MsGraphAccessToken is a plaintext [string] by public contract (populated from workflow
    # secrets/federated tokens in CI pipelines); Connect-MgGraph -AccessToken requires a
    # SecureString, so the one-time in-memory conversion below is unavoidable without a breaking
    # change to the parameter type.
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'MsGraphAccessToken is an existing public string parameter fed by external pipeline secrets; converting it in memory for Connect-MgGraph -AccessToken does not expose a new plaintext secret, and changing the parameter to SecureString would break the public API.')]
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $False)]
        [ValidateSet('UserInteractive', 'SystemAssignedMSI', 'UserAssignedMSI', 'FederatedCredentials', 'AlreadyAuthenticated', 'DeviceAuthentication')]
        [System.String]$AuthenticationType = "AlreadyAuthenticated"
        ,
        [Parameter(Mandatory = $False)]
        [ValidateSet("beta", "v1.0")]
        [System.String]$GraphApiVersion = "beta"
        ,
        [Parameter(Mandatory = $False)]
        [System.String]$AccountId
        ,
        [Parameter(Mandatory = $True)]
        [ValidatePattern('^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$')]
        [System.String]$TenantName
        ,
        [Parameter(Mandatory = $False)]
        [ValidatePattern('^$|^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$')]
        [System.String]$TenantId
        ,
        [Parameter(Mandatory = $False)]
        [ValidatePattern('^$|^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$')]
        [System.String]$ManagingTenantId
        ,
        [Parameter(Mandatory = $False)]
        [System.String]$ManagingTenantName
        ,
        [Parameter(Mandatory = $False)]
        [boolean]$MultiTenantRepo = $false
        ,
        [Parameter(Mandatory = $False)]
        [System.String]$AzArmAccessToken
        ,
        [Parameter(Mandatory = $False)]
        [System.String]$MsGraphAccessToken
        ,
        [Parameter(Mandatory = $False)]
        [ValidateScript({ Test-Path $_ })]
        [System.String]$ConfigFilePath
        ,
        [Parameter(Mandatory = $False)]
        [switch]$NoWelcome
        ,
        [Parameter(Mandatory = $False)]
        [switch]$UseInvokeRestMethodOnly       
    )

    Process {

        $ErrorActionPreference = "Stop"

        # Display welcome banner unless suppressed
        if (-not $NoWelcome) {
            # Robustly resolve module manifest path
            $ManifestPath = if ($MyInvocation.MyCommand.Module.ModuleBase) {
                Join-Path $MyInvocation.MyCommand.Module.ModuleBase "EntraOps.psd1"
            } else {
                # Fallback to relative path if module context is missing (e.g. running script directly)
                "$PSScriptRoot/../../EntraOps.psd1"
            }
            
            if (Test-Path $ManifestPath) {
                $ModuleManifest = Import-PowerShellDataFile $ManifestPath
                $ModuleVersion = $($ModuleManifest.ModuleVersion)
            } else {
                $ModuleVersion = "Unknown"
            }

            $PSEnvironment = "PowerShell " + ($PSVersionTable.PSEdition) + " " + ($PSVersionTable.PSVersion.ToString())

            $Splash = @"
 ______ _ ____
| ____| | | / __ \
| |__ _ __ | |_ _ __ __ _| | | |_ __ ___
| __| | '_ \| __| '__/ _` | | | | '_ \/ __|
| |____| | | | |_| | | (_| | |__| | |_) \__ \
|______|_| |_|\__|_| \__,_|\____/| .__/|___/
                                  | |
                                  |_|
 
Version $($ModuleVersion) on $($PSEnvironment)
Community Project by Thomas Naunheim - www.entraops.com
"@


            Write-Host $Splash -ForegroundColor Blue
        }

        #region Load tenant and managing tenant settings from config file if not provided as parameters
        if ($ConfigFilePath -and (Test-Path $ConfigFilePath)) {
            $EarlyConfig = Get-Content -Path $ConfigFilePath | ConvertFrom-Json
            if ([string]::IsNullOrEmpty($TenantId) -and -not [string]::IsNullOrEmpty($EarlyConfig.TenantId)) {
                $TenantId = $EarlyConfig.TenantId
            }
            if ([string]::IsNullOrEmpty($ManagingTenantId) -and -not [string]::IsNullOrEmpty($EarlyConfig.ManagingTenantId)) {
                $ManagingTenantId = $EarlyConfig.ManagingTenantId
            }
            if ([string]::IsNullOrEmpty($ManagingTenantName) -and -not [string]::IsNullOrEmpty($EarlyConfig.ManagingTenantName)) {
                $ManagingTenantName = $EarlyConfig.ManagingTenantName
            }
        }
        #endregion

        #region Switch between Microsoft Graph SDK (Invoke-MgGraphRequest) and Azure PowerShell only in combination with Invoke-RestMethod
        # Resolve the effective mode: explicit parameter > config file setting > default ($false)
        if (-not $PSBoundParameters.ContainsKey('UseInvokeRestMethodOnly') -and $null -ne $EarlyConfig.UseInvokeRestMethodOnly) {
            $UseInvokeRestMethodOnly = [bool]$EarlyConfig.UseInvokeRestMethodOnly
            Write-Verbose "UseInvokeRestMethodOnly set from config file: $UseInvokeRestMethodOnly"
        }

        # The module-private session store is the authoritative home of the mode - all
        # Invoke-EntraOps*Query cmdlets read it from there (a user-set global variable is
        # only honored as legacy fallback when the session store has no value)
        $__EntraOpsSession['UseInvokeRestMethodOnly'] = [bool]$UseInvokeRestMethodOnly

        # Clean up the flag persisted as global variable by previous module versions
        Remove-Variable -Name UseInvokeRestMethodOnly -Scope Global -Force -ErrorAction SilentlyContinue

        # Start each connection with fresh token state: cached tokens from a previous connection may
        # belong to another tenant and would otherwise be re-used until they expire. Tenant-keyed ARM
        # token entries stay valid; only the context-dependent 'default' entry must go.
        $__EntraOpsSession.MsGraphTokenCache.Clear()
        $__EntraOpsSession.ArmTokenCache.Remove('default')

        # Persist an explicitly provided Microsoft Graph access token in the module-private session
        # store so the Invoke-RestMethod path of Invoke-EntraOpsMsGraphQuery can use it (required for
        # token-based authentication where the Az context cannot mint new Graph tokens itself).
        # Deliberately not a global variable: module scope keeps the token out of the user-visible
        # session state (Get-Variable, transcripts, diagnostic dumps).
        if (-not [string]::IsNullOrEmpty($MsGraphAccessToken)) {
            # Read the real expiry from the JWT exp claim; fall back to a conservative default
            $ProvidedTokenExpiry = [DateTime]::UtcNow.AddMinutes(50)
            try {
                $PayloadPart = ($MsGraphAccessToken -split '\.')[1].Replace('-', '+').Replace('_', '/')
                switch ($PayloadPart.Length % 4) { 2 { $PayloadPart += '==' } 3 { $PayloadPart += '=' } }
                $TokenPayload = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($PayloadPart)) | ConvertFrom-Json
                if ($TokenPayload.exp) { $ProvidedTokenExpiry = [DateTimeOffset]::FromUnixTimeSeconds($TokenPayload.exp).UtcDateTime }
            } catch {
                Write-Verbose "Could not parse expiry from provided Microsoft Graph access token: $($_.Exception.Message)"
            }

            $__EntraOpsSession.MsGraphTokenCache['provided'] = @{ Token = $MsGraphAccessToken; Expiry = $ProvidedTokenExpiry }

            # Clean up a token persisted as global variable by previous module versions
            Remove-Variable -Name MsGraphAccessToken -Scope Global -Force -ErrorAction SilentlyContinue
        }

        # Azure authentication and ARM access are required in every connection mode.
        @(
            @{ ModuleName = 'Az.Accounts'; ModuleVersion = '2.19.0' }
            @{ ModuleName = 'Az.Resources'; ModuleVersion = '6.16.2' }
        ) | ForEach-Object {
            Install-EntraOpsRequiredModule -ModuleName $_.ModuleName -MinimalVersion $_.ModuleVersion
        }

        # The Microsoft Graph SDK is only required (and installed) when it is actually used
        if (-not $__EntraOpsSession['UseInvokeRestMethodOnly']) {
            $RequiredCoreModules = @{
                ModuleName    = 'Microsoft.Graph.Authentication'
                ModuleVersion = '2.0.0'
            }
            # Recommendation 1: Validate module availability before installation check
            $RequiredCoreModules | ForEach-Object {
                if (-not (Get-Module -Name $_.ModuleName)) {
                    Install-EntraOpsRequiredModule -ModuleName $_.ModuleName -MinimalVersion $_.ModuleVersion
                }
            }

            $Scopes = @(
                "AdministrativeUnit.Read.All",
                "Application.Read.All",
                "CustomSecAttributeAssignment.Read.All",
                "DeviceManagementConfiguration.Read.All",
                "DeviceManagementManagedDevices.Read.All",
                "DeviceManagementRBAC.Read.All",
                "DeviceManagementServiceConfig.Read.All",
                "Directory.Read.All",
                "DirectoryRecommendations.Read.All",
                "EntitlementManagement.Read.All",
                "Group.Read.All",
                "PrivilegedAccess.Read.AzureADGroup",
                "PrivilegedEligibilitySchedule.Read.AzureADGroup",
                "Policy.Read.All",
                "RemoteTenantGroups.Read.All",
                "RoleManagement.Read.All",
                "TenantGovernance-Relationship.Read.All",
                "ThreatHunting.Read.All",
                "User.Read.All",
                "Zone.Read.All"
            )
        }
        #endregion

        # In UseInvokeRestMethodOnly mode, Invoke-EntraOps*Query cmdlets acquire and cache their own
        # Microsoft Graph token via Get-AzAccessToken, so Connect-MgGraph is unnecessary for the
        # service-principal/managed-identity authentication types below: their Graph permissions are
        # Application permissions already admin-consented to the app, and a client-credentials token
        # always contains every granted app role (no per-request scope negotiation needed).
        # UserInteractive/DeviceAuthentication are the exception: Get-AzAccessToken cannot trigger the
        # interactive/incremental consent prompt that delegated Graph scopes require, so those two
        # authentication types still fall back to Connect-MgGraph (and therefore still require
        # Microsoft.Graph.Authentication to be installed) even when UseInvokeRestMethodOnly is enabled.
        $SkipGraphSdkAuth = [bool]$__EntraOpsSession['UseInvokeRestMethodOnly'] -and ($AuthenticationType -notin @('UserInteractive', 'DeviceAuthentication'))
        if ($__EntraOpsSession['UseInvokeRestMethodOnly'] -and $AuthenticationType -in @('UserInteractive', 'DeviceAuthentication')) {
            Write-Warning "UseInvokeRestMethodOnly is enabled, but AuthenticationType '$AuthenticationType' still requires Connect-MgGraph (Microsoft.Graph.Authentication module) as a fallback: Get-AzAccessToken cannot trigger the interactive/incremental admin-consent prompt needed for delegated Microsoft Graph scopes, only Connect-MgGraph -Scopes can. Use a service-principal-based AuthenticationType (SystemAssignedMSI, UserAssignedMSI, FederatedCredentials or AlreadyAuthenticated) to avoid this Graph SDK dependency, or provide an already-consented -MsGraphAccessToken."
        }

        #region Switch to choose authentication method for Azure and Microsoft Graph
        switch ( $AuthenticationType ) {
            UserInteractive {
                try {
                    # Pre-authenticate to managing tenant for cross-tenant access
                    if (-not [string]::IsNullOrEmpty($ManagingTenantId)) {
                        Write-Output "Pre-authenticating to managing tenant (Azure)..."
                        Connect-AzAccount -Tenant $ManagingTenantId -ErrorAction Stop | Out-Null
                        Write-Output "Pre-authenticating to managing tenant (Microsoft Graph) using Az token..."
                        $SecureManagingAccessToken = (Get-AzAccessToken -ResourceTypeName "MSGraph" -TenantId $ManagingTenantId -AsSecureString -ErrorAction Stop).Token
                        Connect-MgGraph -AccessToken $SecureManagingAccessToken -NoWelcome -ErrorAction Stop
                        Write-Output "Successfully pre-authenticated to managing tenant $ManagingTenantId"
                    }

                    Write-Output "Logging in to Azure..."
                    Connect-AzAccount -Tenant $TenantName -ErrorAction Stop | Out-Null
                    if ($TenantId -ne (Get-AzContext).Tenant.Id -or $null -eq $TenantId) {
                        $TenantId = (Get-AzContext).Tenant.Id
                    }
                    Write-Output "Succesfully logged in to Azure"
                    Write-Output "Logging in to Microsoft Graph..."
                    Connect-MgGraph -TenantId $TenantId -NoWelcome -ErrorAction Stop -Scopes $Scopes
                    Write-Output "Succesfully logged in to Microsoft Graph"
                } catch {
                    Write-Error -Message $_.Exception
                    throw $_.Exception
                }
            }
            DeviceAuthentication {
                try {
                    # Pre-authenticate to managing tenant for cross-tenant access
                    if (-not [string]::IsNullOrEmpty($ManagingTenantId)) {
                        Write-Output "Pre-authenticating to managing tenant (Azure)..."
                        Connect-AzAccount -Tenant $ManagingTenantId -ErrorAction Stop -UseDeviceAuthentication | Out-Null
                        Write-Output "Pre-authenticating to managing tenant (Microsoft Graph) using Az token..."
                        $SecureManagingAccessToken = (Get-AzAccessToken -ResourceTypeName "MSGraph" -TenantId $ManagingTenantId -AsSecureString -ErrorAction Stop).Token
                        Connect-MgGraph -AccessToken $SecureManagingAccessToken -NoWelcome -ErrorAction Stop
                        Write-Output "Successfully pre-authenticated to managing tenant $ManagingTenantId"
                    }

                    Write-Output "Logging in to Azure..."
                    Connect-AzAccount -Tenant $TenantName -ErrorAction Stop -UseDeviceAuthentication | Out-Null
                    if ($TenantId -ne (Get-AzContext).Tenant.Id -or $null -eq $TenantId) {
                        $TenantId = (Get-AzContext).Tenant.Id
                    }
                    Write-Output "Succesfully logged in to Azure"
                    Write-Output "Logging in to Microsoft Graph..."
                    Connect-MgGraph -TenantId $TenantId -NoWelcome -ErrorAction Stop -Scopes $Scopes -UseDeviceAuthentication
                    Write-Output "Succesfully logged in to Microsoft Graph"
                } catch {
                    Write-Error -Message $_.Exception
                    throw $_.Exception
                }
            }
            SystemAssignedMSI {
                try {
                    Write-Output "Logging in to Azure..."
                    Connect-AzAccount -Identity -ErrorAction Stop
                    Write-Output "Succesfully logged in to Azure"
                    if (-not $SkipGraphSdkAuth) {
                        Write-Output "Logging in to Microsoft Graph..."
                        Connect-MgGraph -Identity -ErrorAction Stop -NoWelcome
                        Write-Output "Succesfully logged in to Microsoft Graph"
                    } else {
                        Write-Verbose "UseInvokeRestMethodOnly is enabled: skipping Connect-MgGraph, Microsoft Graph tokens will be acquired via Get-AzAccessToken when needed."
                    }
                } catch {
                    Write-Error -Message $_.Exception
                    throw $_.Exception
                }
            }
            UserAssignedMSI {
                try {
                    Write-Output "Logging in to Azure..."
                    Connect-AzAccount -Identity -AccountId $AccountId -ErrorAction Stop
                    Write-Output "Succesfully logged in to Azure"
                    if (-not $SkipGraphSdkAuth) {
                        Write-Output "Logging in to Microsoft Graph..."
                        Connect-MgGraph -Identity -ClientId $AccountId -NoWelcome -ErrorAction Stop
                        Write-Output "Succesfully logged in to Microsoft Graph"
                    } else {
                        Write-Verbose "UseInvokeRestMethodOnly is enabled: skipping Connect-MgGraph, Microsoft Graph tokens will be acquired via Get-AzAccessToken when needed."
                    }
                } catch {
                    Write-Error -Message $_.Exception
                    throw $_.Exception
                }
            }
            FederatedCredentials {
                if ($Null -eq (Get-AzContext).Tenant.Id) {
                    throw "Federated environment is not already authenticated"
                }
                try {
                    # Pre-authenticate to managing tenant for cross-tenant access
                    if (-not [string]::IsNullOrEmpty($ManagingTenantId)) {
                        $SecureManagingAccessToken = (Get-AzAccessToken -ResourceTypeName "MSGraph" -TenantId $ManagingTenantId -AsSecureString).Token
                        if (-not $SkipGraphSdkAuth) {
                            Write-Output "Pre-authenticating to managing tenant (Microsoft Graph)..."
                            Connect-MgGraph -AccessToken $SecureManagingAccessToken -ErrorAction Stop -NoWelcome
                            Write-Output "Successfully pre-authenticated to managing tenant $ManagingTenantId"
                        }
                    }

                    # Connect to target tenant
                    if (-not [string]::IsNullOrEmpty($TenantId)) {
                        $SecureAccessToken = (Get-AzAccessToken -ResourceTypeName "MSGraph" -TenantId $TenantId -AsSecureString).Token
                    } else {
                        $SecureAccessToken = (Get-AzAccessToken -ResourceTypeName "MSGraph" -AsSecureString).Token
                    }
                    if (-not $SkipGraphSdkAuth) {
                        Connect-MgGraph -AccessToken $SecureAccessToken -ErrorAction Stop -NoWelcome
                    } else {
                        Write-Verbose "UseInvokeRestMethodOnly is enabled: skipping Connect-MgGraph, Microsoft Graph tokens will be acquired via Get-AzAccessToken when needed."
                    }

                    # Pre-warm an ARM access token too (same pattern as above), while the GitHub OIDC
                    # federated assertion behind this Az context is still fresh (it's only valid for a
                    # few minutes). Az.Accounts caches acquired tokens per resource/tenant across the
                    # job's remaining steps, so a later step's first-ever ARM request (e.g. Azure RBAC
                    # collection) is served from this cached token instead of needing a new assertion
                    # exchange once it has expired. Non-fatal: callers that never touch Azure RBAC
                    # should not fail Connect-EntraOps over this optimization.
                    try {
                        if (-not [string]::IsNullOrEmpty($TenantId)) {
                            $SecureArmAccessToken = (Get-AzAccessToken -ResourceUrl "https://management.azure.com/" -TenantId $TenantId -AsSecureString).Token
                        } else {
                            $SecureArmAccessToken = (Get-AzAccessToken -ResourceUrl "https://management.azure.com/" -AsSecureString).Token
                        }
                    } catch {
                        Write-Verbose "Could not pre-warm an Azure Resource Manager access token: $($_.Exception.Message)"
                    }
                } catch {
                    throw $_.Exception
                }
            }
            AlreadyAuthenticated {
                # Recommendation 2: Optimize context retrieval to avoid redundant cmdlet calls
                $CurrentAzContext = Get-AzContext
                $CurrentMgContext = if (-not $SkipGraphSdkAuth) { Get-MgContext } else { $null }

                # Pre-authenticate to managing tenant for cross-tenant access
                if (-not [string]::IsNullOrEmpty($ManagingTenantId) -and $Null -ne $CurrentAzContext.Tenant.Id) {
                    try {
                        $SecureManagingAccessToken = (Get-AzAccessToken -ResourceTypeName "MSGraph" -TenantId $ManagingTenantId -AsSecureString).Token
                        if (-not $SkipGraphSdkAuth) {
                            Write-Output "Pre-authenticating to managing tenant (Microsoft Graph)..."
                            Connect-MgGraph -AccessToken $SecureManagingAccessToken -ErrorAction Stop -NoWelcome
                            Write-Output "Successfully pre-authenticated to managing tenant $ManagingTenantId"
                        }
                    } catch {
                        Write-Warning "Failed to pre-authenticate to managing tenant: $($_.Exception.Message)"
                    }
                }

                if ($AccountId -and $MsGraphAccessToken -and $AzArmAccessToken) {
                    Connect-AzAccount -AccountId $AccountId -AccessToken $AzArmAccessToken -Tenant $TenantName

                    if (-not $SkipGraphSdkAuth) {
                        $SecureMsGraphAccessToken = $MsGraphAccessToken | ConvertTo-SecureString -AsPlainText -Force
                        Connect-MgGraph -AccessToken $SecureMsGraphAccessToken -NoWelcome
                    }

                } elseif ($Null -ne $CurrentAzContext.Tenant.Id -and $Null -ne $CurrentMgContext.TenantId) {
                    try {
                        $SecureAccessToken = (Get-AzAccessToken -ResourceTypeName "MSGraph" -AsSecureString).Token
                        Connect-MgGraph -AccessToken $SecureAccessToken -ErrorAction Stop -NoWelcome
                    } catch {
                        $ErrorMessage = if ($null -ne $_.Exception.Message) { $_.Exception.Message } else { $_.ToString() }
                        throw "Failed to connect to Microsoft Graph using Azure access token: $ErrorMessage"
                    }                    
                } elseif ($Null -ne $CurrentAzContext.Tenant.Id) {
                    if ($SkipGraphSdkAuth) {
                        Write-Verbose "UseInvokeRestMethodOnly is enabled: skipping Connect-MgGraph, Microsoft Graph tokens will be acquired via Get-AzAccessToken when needed."
                    } else {
                        try {
                            $SecureAccessToken = (Get-AzAccessToken -ResourceTypeName "MSGraph" -AsSecureString).Token
                            Connect-MgGraph -AccessToken $SecureAccessToken -ErrorAction Stop -NoWelcome
                        } catch {
                            $ErrorMessage = if ($null -ne $_.Exception.Message) { $_.Exception.Message } else { $_.ToString() }
                            throw "Failed to connect to Microsoft Graph using Azure access token: $ErrorMessage"
                        }
                    }
                } else {
                    Write-Error -Message 'User or workload is not already authenticated. This authentication method is the default for EntraOps. Check "Get-Help Connect-EntraOps" to review the various options. Authenticated Azure PowerShell session is required for using "AlreadyAuthenticated" mode.'
                }
            }
        }
        #endregion

        #region Verify connection to target tenant
        if (-not [string]::IsNullOrEmpty($TenantId)) {
            $VerifyAzContext = Get-AzContext
            if ($VerifyAzContext -and $VerifyAzContext.Tenant.Id -ne $TenantId) {
                Write-Output "Azure context is on tenant $($VerifyAzContext.Tenant.Id), switching to target tenant $TenantId..."
                Set-AzContext -TenantId $TenantId -ErrorAction Stop | Out-Null
                Write-Output "Successfully switched Azure context to target tenant"
            }

            $VerifyMgContext = if (-not $SkipGraphSdkAuth) { Get-MgContext } else { $null }
            if ($VerifyMgContext -and $VerifyMgContext.TenantId -ne $TenantId) {
                Write-Output "Microsoft Graph is connected to $($VerifyMgContext.TenantId), reconnecting to target tenant $TenantId..."
                try {
                    $SecureTargetAccessToken = (Get-AzAccessToken -ResourceTypeName "MSGraph" -TenantId $TenantId -AsSecureString).Token
                    Connect-MgGraph -AccessToken $SecureTargetAccessToken -ErrorAction Stop -NoWelcome
                    Write-Output "Successfully reconnected Microsoft Graph to target tenant"
                } catch {
                    Write-Warning "Could not reconnect Microsoft Graph to target tenant ${TenantId}: $($_.Exception.Message)"
                }
            }
        }
        #endregion

        #region Summary of established connection to ARM and Microsoft Graph API
        # Recommendation 3: Optimize context retrieval and verbose output generation
        Write-Verbose -Message "Connected to Azure Management"
        $AzContextRaw = Get-AzContext
        $AzContext = $AzContextRaw | Select-Object Account, Tenant, TokenCache
        Write-Verbose ($AzContext | Out-String)

        # Retrieve MG context once (skipped when Connect-MgGraph was itself skipped in REST-only mode)
        $MgContextRaw = if (-not $SkipGraphSdkAuth) { Get-MgContext } else { $null }

        if ($MgContextRaw) {
            Write-Verbose -Message "Connected to Microsoft Graph"
        } else {
            Write-Verbose -Message "Not connected via Microsoft Graph SDK (UseInvokeRestMethodOnly mode: tokens are acquired via Get-AzAccessToken as needed)"
        }
        $MgContext = $MgContextRaw | Select-Object ClientId, TenantId, AppName, ContextScope
        Write-Verbose -Message ($MgContext | Out-String)

        Write-Verbose "Scoped permissions in Microsoft Graph"
        $MgScopes = $MgContextRaw.Scopes
        Write-Verbose -Message ($MgScopes | Out-String)
        #endregion

        #region Validate baseline Graph scopes for already-authenticated user sessions
        if ($AuthenticationType -eq 'AlreadyAuthenticated' -and $AzContextRaw.Account.Type -eq 'User' -and $MgContextRaw) {
            $MissingScopes = @($Scopes | Where-Object { $_ -notin $MgContextRaw.Scopes })
            $TenantGovernanceRelationshipScope = 'TenantGovernance-Relationship.Read.All'
            $MissingBaselineScopes = @($MissingScopes | Where-Object { $_ -ne $TenantGovernanceRelationshipScope })
            if ($MissingBaselineScopes.Count -gt 0) {
                Write-Warning "The already-authenticated user session is missing $($MissingBaselineScopes.Count) baseline EntraOps delegated scope(s): $($MissingBaselineScopes -join ', '). Dependent collection steps may fail with 403 Forbidden. Reconnect with the missing scopes consented."
            }
            if ($MissingScopes -contains $TenantGovernanceRelationshipScope) {
                $HasManagingTenantConfiguration = -not [string]::IsNullOrWhiteSpace($ManagingTenantId) -or -not [string]::IsNullOrWhiteSpace($ManagingTenantName)
                if ($HasManagingTenantConfiguration) {
                    Write-Warning "The already-authenticated user session is missing '$TenantGovernanceRelationshipScope'. Tenant Governance delegated-administration relationships may not be discovered or analyzed. Reconnect with this delegated scope consented."
                } else {
                    Write-Warning "The already-authenticated user session is missing '$TenantGovernanceRelationshipScope'. No ManagingTenantId or ManagingTenantName is configured, so this may be acceptable only if you have verified that no Tenant Governance delegated-administration relationships exist. Without this scope, unknown relationships cannot be discovered or analyzed."
                }
            }
        }
        #endregion

        #region Import Environment variables if exists
        $IncludeObjectDetails = $false
        if ($ConfigFilePath) {
            try {
                $EntraOpsConfig = Get-Content -Path $ConfigFilePath | ConvertFrom-Json -Depth 10 -AsHashtable
            } catch {
                Write-Error "Issue to import config file $($ConfigFilePath)! Check if the file exists and is in JSON format."
            }
            # Remove Ingest or Apply* parameters from config file and show configuration
            $EntraOpsConfig.AutomatedClassificationUpdate.Remove("ApplyAutomatedClassificationUpdate")
            $EntraOpsConfig.AutomatedControlPlaneScopeUpdate.Remove("ApplyAutomatedControlPlaneScopeUpdate")
            if ($EntraOpsConfig.LogAnalytics) { $EntraOpsConfig.LogAnalytics.Remove("IngestToLogAnalytics") }
            if ($EntraOpsConfig.SentinelWatchLists) { $EntraOpsConfig.SentinelWatchLists.Remove("IngestToWatchLists") }
            if ($EntraOpsConfig.AutomatedAdministrativeUnitManagement) { $EntraOpsConfig.AutomatedAdministrativeUnitManagement.Remove("ApplyAdministrativeUnitAssignments") }
            if ($EntraOpsConfig.AutomatedConditionalAccessTargetGroups) { $EntraOpsConfig.AutomatedConditionalAccessTargetGroups.Remove("ApplyConditionalAccessTargetGroups") }
            if ($EntraOpsConfig.AutomatedRmauAssignmentsForUnprotectedObjects) { $EntraOpsConfig.AutomatedRmauAssignmentsForUnprotectedObjects.Remove("ApplyRmauAssignmentsForUnprotectedObjects") }
            if ($EntraOpsConfig.AutomatedElmCatalogProtection) { $EntraOpsConfig.AutomatedElmCatalogProtection.Remove("ApplyPrivilegedElmCatalogProtection") }
            if ($EntraOpsConfig.ConsoleOutput -and $null -ne $EntraOpsConfig.ConsoleOutput.IncludeObjectDetails) {
                $IncludeObjectDetails = [bool]$EntraOpsConfig.ConsoleOutput.IncludeObjectDetails
            }

            New-Variable -Name EntraOpsConfig -Value $EntraOpsConfig -Scope Global -Force
            Write-Verbose -Message "Config file $($ConfigFilePath) imported"
        }
        #endregion

        #region Set global variables
        New-Variable -Name TenantIdContext -Value $TenantId -Scope Global -Force
        New-Variable -Name TenantNameContext -Value $TenantName -Scope Global -Force
        New-Variable -Name ManagingTenantIdContext -Value $ManagingTenantId -Scope Global -Force
        New-Variable -Name ManagingTenantNameContext -Value $ManagingTenantName -Scope Global -Force
        New-Variable -Name EntraOpsIncludeObjectDetails -Value $IncludeObjectDetails -Scope Global -Force
        New-Variable -Name XdrAvdHuntingAccess -Value (-not $SkipGraphSdkAuth -and (Get-MgContext).Scopes -contains "ThreatHunting.Read.All") -Scope Global -Force
        $__EntraOpsSession['AuthenticationType'] = $AuthenticationType
        # $DefaultFolderClassification is always the Classification/ root; consumers append
        # $TenantNameContext or Templates/ themselves (Resolve-EntraOpsClassificationPath,
        # Import-EntraOpsClassificationOverwrites, Update-EntraOpsClassificationControlPlaneScope, ...).
        New-Variable -Name DefaultFolderClassification -Value "$EntraOpsBaseFolder/Classification/" -Scope Global -Force
        if ($MultiTenantRepo -eq $true) {
            New-Variable -Name DefaultFolderClassifiedEam -Value "$EntraOpsBaseFolder/PrivilegedEAM/$($TenantName)/" -Scope Global -Force
            Write-Verbose -Message "Multi Tenant in Repository"
        } else {
            New-Variable -Name DefaultFolderClassifiedEam -Value "$EntraOpsBaseFolder/PrivilegedEAM/" -Scope Global -Force
            Write-Verbose -Message "Single Tenant in Repository"
        }
        #endregion

        if (-not $NoWelcome) {
            #region Display connection summary
            Write-Host ""
            Write-Host "═══════════════════════════════════════════════════════════════════════════════" -ForegroundColor Cyan
            Write-Host " 🔐 Connection Summary" -ForegroundColor Cyan
            Write-Host "═══════════════════════════════════════════════════════════════════════════════" -ForegroundColor Cyan
            
            Write-Host " Authentication Type : $AuthenticationType" -ForegroundColor White

            # Display managing tenant info if configured
            if (-not [string]::IsNullOrEmpty($ManagingTenantId)) {
                Write-Host " Managing Tenant ID : $ManagingTenantId" -ForegroundColor White
                Write-Host " Managing Tenant Name: $ManagingTenantName" -ForegroundColor White
            }

            # Get Azure context
            $AzContext = Get-AzContext -ErrorAction SilentlyContinue
            if ($AzContext) {
                Write-Host " Azure Account : $($AzContext.Account.Id)" -ForegroundColor Green
                Write-Host " Azure Tenant : $($AzContext.Tenant.Id)" -ForegroundColor White
                Write-Host " Azure Subscription : $($AzContext.Subscription.Name)" -ForegroundColor White
            } else {
                Write-Host " Azure Account : Not connected" -ForegroundColor Gray
            }
            
            # Get Microsoft Graph context
            $MgContext = if (-not $SkipGraphSdkAuth) { Get-MgContext } else { $null }
            if ($MgContext) {
                Write-Host " Graph Account : $($MgContext.Account)" -ForegroundColor Green
                Write-Host " Graph Tenant : $($MgContext.TenantId)" -ForegroundColor White
                Write-Host " Graph Auth Type : $($MgContext.AuthType)" -ForegroundColor White
                
                # Display scopes (truncate if too many)
                $Scopes = $MgContext.Scopes
                if ($Scopes.Count -gt 0) {
                    $ScopeDisplay = if ($Scopes.Count -le 5) {
                        $Scopes -join ", "
                    } else {
                        ($Scopes | Select-Object -First 5) -join ", " + " (+$($Scopes.Count - 5) more)"
                    }
                    Write-Host " Graph Scopes : $ScopeDisplay" -ForegroundColor White
                }
            } elseif ($SkipGraphSdkAuth) {
                Write-Host " Graph Account : Not connected (UseInvokeRestMethodOnly - tokens acquired via Get-AzAccessToken as needed)" -ForegroundColor Gray
            } else {
                Write-Host " Graph Account : Not connected" -ForegroundColor Gray
            }
            
            Write-Host "═══════════════════════════════════════════════════════════════════════════════" -ForegroundColor Cyan
            Write-Host ""
            #endregion
            
            #region Display cache information
            Write-Host "═══════════════════════════════════════════════════════════════════════════════" -ForegroundColor Cyan
            Write-Host " 📦 Cache Configuration" -ForegroundColor Cyan
            Write-Host "═══════════════════════════════════════════════════════════════════════════════" -ForegroundColor Cyan
            
            # Cache location
            Write-Host " Cache Location : $($__EntraOpsSession.PersistentCachePath)" -ForegroundColor White
            
            # Memory cache
            $MemoryCacheCount = $__EntraOpsSession.GraphCache.Count
            Write-Host " Memory Cache : $MemoryCacheCount entries" -ForegroundColor $(if ($MemoryCacheCount -gt 0) { "Green" } else { "Gray" })
            
            # Persistent cache statistics
            if (Test-Path $__EntraOpsSession.PersistentCachePath) {
                $PersistentFiles = Get-ChildItem -Path $__EntraOpsSession.PersistentCachePath -Filter "*.json" -ErrorAction SilentlyContinue
                $PersistentCount = $PersistentFiles.Count
                
                if ($PersistentCount -gt 0) {
                    $PersistentSizeMB = [Math]::Round(($PersistentFiles | Measure-Object -Property Length -Sum).Sum / 1MB, 2)
                    Write-Host " Persistent Cache : $PersistentCount files ($PersistentSizeMB MB)" -ForegroundColor Green
                    
                    # Show newest cache file
                    $NewestCache = $PersistentFiles | Sort-Object LastWriteTime -Descending | Select-Object -First 1
                    if ($NewestCache) {
                        $CacheAge = [Math]::Round(((Get-Date) - $NewestCache.LastWriteTime).TotalHours, 1)
                        Write-Host " Latest Cache Update : $CacheAge hours ago" -ForegroundColor $(if ($CacheAge -lt 1) { "Green" } elseif ($CacheAge -lt 24) { "Yellow" } else { "Gray" })
                    }
                } else {
                    Write-Host " Persistent Cache : Empty (will populate on first API call)" -ForegroundColor Gray
                }
            } else {
                Write-Host " Persistent Cache : Directory will be created on first use" -ForegroundColor Gray
            }
            
            # Cache TTL settings
            Write-Host " Default TTL : $([Math]::Round($__EntraOpsSession.DefaultCacheTTL / 3600, 1)) hours" -ForegroundColor White
            Write-Host " Static Data TTL : $([Math]::Round($__EntraOpsSession.StaticDataCacheTTL / 3600, 1)) hours" -ForegroundColor White
            
            Write-Host "═══════════════════════════════════════════════════════════════════════════════" -ForegroundColor Cyan
            Write-Host ""
            
            # Warning to disconnect when finished
            Write-Host "⚠️ REMINDER: Run 'Disconnect-EntraOps' when finished to:" -ForegroundColor Yellow
            Write-Host " • Clear persistent cache on disk with results from your session and free memory" -ForegroundColor Yellow
            Write-Host " • Disconnect from Azure Resource Management" -ForegroundColor Yellow
            Write-Host " • Disconnect from Microsoft Graph API" -ForegroundColor Yellow
            Write-Host ""
            #endregion
        }
    }
}
# SIG # Begin signature block
# MIIniAYJKoZIhvcNAQcCoIIneTCCJ3UCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCC+VTjlwmFwI5vq
# QMfrjxsmiNCmDylMOggg6+T17Cyie6CCIDwwggXJMIIEsaADAgECAhAbtY8lKt8j
# AEkoya49fu0nMA0GCSqGSIb3DQEBDAUAMH4xCzAJBgNVBAYTAlBMMSIwIAYDVQQK
# ExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2Vy
# dGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBUcnVzdGVkIE5l
# dHdvcmsgQ0EwHhcNMjEwNTMxMDY0MzA2WhcNMjkwOTE3MDY0MzA2WjCBgDELMAkG
# A1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMuQS4xJzAl
# BgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIGA1UEAxMb
# Q2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMIICIjANBgkqhkiG9w0BAQEFAAOC
# Ag8AMIICCgKCAgEAvfl4+ObVgAxknYYblmRnPyI6HnUBfe/7XGeMycxca6mR5rlC
# 5SBLm9qbe7mZXdmbgEvXhEArJ9PoujC7Pgkap0mV7ytAJMKXx6fumyXvqAoAl4Va
# qp3cKcniNQfrcE1K1sGzVrihQTib0fsxf4/gX+GxPw+OFklg1waNGPmqJhCrKtPQ
# 0WeNG0a+RzDVLnLRxWPa52N5RH5LYySJhi40PylMUosqp8DikSiJucBb+R3Z5yet
# /5oCl8HGUJKbAiy9qbk0WQq/hEr/3/6zn+vZnuCYI+yma3cWKtvMrTscpIfcRnNe
# GWJoRVfkkIJCu0LW8GHgwaM9ZqNd9BjuiMmNF0UpmTJ1AjHuKSbIawLmtWJFfzcV
# WiNoidQ+3k4nsPBADLxNF8tNorMe0AZa3faTz1d1mfX6hhpneLO/lv403L3nUlbl
# s+V1e9dBkQXcXWnjlQ1DufyDljmVe2yAWk8TcsbXfSl6RLpSpCrVQUYJIP4ioLZb
# MI28iQzV13D4h1L92u+sUS4Hs07+0AnacO+Y+lbmbdu1V0vc5SwlFcieLnhO+Nqc
# noYsylfzGuXIkosagpZ6w7xQEmnYDlpGizrrJvojybawgb5CAKT41v4wLsfSRvbl
# jnX98sy50IdbzAYQYLuDNbdeZ95H7JlI8aShFf6tjGKOOVVPORa5sWOd/7cCAwEA
# AaOCAT4wggE6MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLahVDkCw6A/joq8
# +tT4HKbROg79MB8GA1UdIwQYMBaAFAh2zcsH/yT2xc3tu5C84oQ3RnX3MA4GA1Ud
# DwEB/wQEAwIBBjAvBgNVHR8EKDAmMCSgIqAghh5odHRwOi8vY3JsLmNlcnR1bS5w
# bC9jdG5jYS5jcmwwawYIKwYBBQUHAQEEXzBdMCgGCCsGAQUFBzABhhxodHRwOi8v
# c3ViY2Eub2NzcC1jZXJ0dW0uY29tMDEGCCsGAQUFBzAChiVodHRwOi8vcmVwb3Np
# dG9yeS5jZXJ0dW0ucGwvY3RuY2EuY2VyMDkGA1UdIAQyMDAwLgYEVR0gADAmMCQG
# CCsGAQUFBwIBFhhodHRwOi8vd3d3LmNlcnR1bS5wbC9DUFMwDQYJKoZIhvcNAQEM
# BQADggEBAFHCoVgWIhCL/IYx1MIy01z4S6Ivaj5N+KsIHu3V6PrnCA3st8YeDrJ1
# BXqxC/rXdGoABh+kzqrya33YEcARCNQOTWHFOqj6seHjmOriY/1B9ZN9DbxdkjuR
# mmW60F9MvkyNaAMQFtXx0ASKhTP5N+dbLiZpQjy6zbzUeulNndrnQ/tjUoCFBMQl
# lVXwfqefAcVbKPjgzoZwpic7Ofs4LphTZSJ1Ldf23SIikZbr3WjtP6MZl9M7JYjs
# NhI9qX7OAo0FmpKnJ25FspxihjcNpDOO16hO0EoXQ0zF8ads0h5YbBRRfopUofbv
# n3l6XYGaFpAP4bvxSgD5+d2+7arszgowggZrMIIEU6ADAgECAhBX5gfP8LabvnxC
# XoItEBa/MA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhB
# c3NlY28gRGF0YSBTeXN0ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBDb2RlIFNp
# Z25pbmcgMjAyMSBDQTAeFw0yNjEwMDYwODI4MjNaFw0yNzEwMDYwODI4MjJaMIGQ
# MQswCQYDVQQGEwJERTEdMBsGA1UECAwUUmhpbmVsYW5kLVBhbGF0aW5hdGUxEjAQ
# BgNVBAcMCUxhaG5zdGVpbjEeMBwGA1UECgwVT3BlbiBTb3VyY2UgRGV2ZWxvcGVy
# MS4wLAYDVQQDDCVPcGVuIFNvdXJjZSBEZXZlbG9wZXIgVGhvbWFzIE5hdW5oZWlt
# MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAphp2ljSmVsRB6gvkEq7A
# DUMENpKZiDFoAfF1ktD1nCwaVyI+bJ0ROjHwAn4iJgS+vlBDPOUIw2GLHkiqUhOg
# HSwzg6gj45GNmt6zxsKP0zPlhMB/fb0zmz7/F7e3yfKCXLfhbHuk0Es9gJ+8Wmet
# 9StVwwSE/K0MpW06GKA9nPWyE4N/ADwigDIj9rsTKJR4cve9w8G1b3zohpUc/qMr
# onab+HVw5r9b3sOBnnaXC6zvrX4fovRz1ztwfWjRjILLoFG6mW/TuK5F2ICD3Rhl
# 9R2rJWWWhzTDMsQlzvX+/SaM74q3OFpIVNdhvtmWU9pKUBRAyYMijcDqKEFGSzrc
# 8JFmGMpP5TuroWF+N21d3px2eARvW1mV0DCfFAH+Og20SH/gVW6/ltQQEkH/z/Zt
# 49ARyJrgESLOQtlMCIYSoKISdkk4ml4VUS1E/PcbzXEYj3a7m6DOxcd3WuY03UT9
# cnk3p0LMN6LeLYAZgVY+HRloZlH+kW2SkOHNuWgtXvZhAgMBAAGjggF4MIIBdDAM
# BgNVHRMBAf8EAjAAMD0GA1UdHwQ2MDQwMqAwoC6GLGh0dHA6Ly9jY3NjYTIwMjEu
# Y3JsLmNlcnR1bS5wbC9jY3NjYTIwMjEuY3JsMHMGCCsGAQUFBwEBBGcwZTAsBggr
# BgEFBQcwAYYgaHR0cDovL2Njc2NhMjAyMS5vY3NwLWNlcnR1bS5jb20wNQYIKwYB
# BQUHMAKGKWh0dHA6Ly9yZXBvc2l0b3J5LmNlcnR1bS5wbC9jY3NjYTIwMjEuY2Vy
# MB8GA1UdIwQYMBaAFN10XUwA23ufoHTKsW73PMAywHDNMB0GA1UdDgQWBBTBvGyH
# MXj1dMbVnxuof9oP4PY7cDBLBgNVHSAERDBCMAgGBmeBDAEEATA2BgsqhGgBhvZ3
# AgUBBDAnMCUGCCsGAQUFBwIBFhlodHRwczovL3d3dy5jZXJ0dW0ucGwvQ1BTMBMG
# A1UdJQQMMAoGCCsGAQUFBwMDMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQsF
# AAOCAgEAkC165c7/r6y3VeEEpNXas9bQs6ZpKPdJffHkf9xoPi4kWvAIWGc4ocG1
# DJLb18RXiDYShDmT5sb/6I08FeipKi2DNJkuOq6yOTJ3S7+hNFIZCyd6i7WKO8ea
# P5I+FAbyJP4+5coZoMg45bUa9JoocaJCm+5n7WiCfN7/EQpClW5Trm4+ZMbp7EMN
# F1uLJgGCMnN0SsoUarp7PHlqupMxMC4Uhhe+lAP2ytcSIU81L97/qjrADbLWqICk
# 9il5iGM+aZndguq/cmfH9rJj4GRZHWnG6WwnzYD9+KgYlRI6em+/9dq+IRiXe2Bs
# thUuIaFW7U2gWw8tBTEYm1UBLHdJx+L1gjJCLs5KT/wy5wU32wZxNDqPUQejUC1x
# GvU3cKqmLgRq+yNf3gFVfzqIoggyM3U9wQvnk7nBtCkpjXS2hdx4yxzcmrlWsDHr
# z6+6/jeyQySzbYUlFmKnc4n9Mw2soHOajEua5poFARVRS5KgUBuiiQ21YFNYzWBo
# qGuECaaAI/C5rGxapMJ3dAOBnHw6NLqBet+ZXBJEccX5y89MLlOQu+jDmJP8HieR
# qDLlwfhVZnw4RY8Rus43NZBwhNlVuOUXqocHKmJhKNlekda3qsJj93eQBQOxKJcc
# w5O08q9jukpH2+yzFAj+7ll33DuNUgWptogc4Awtbx3glzcEvJQwggaCMIIEaqAD
# AgECAhAo8HfBHDa9/l90MkdwJy4DMA0GCSqGSIb3DQEBDAUAMFYxCzAJBgNVBAYT
# AlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0ZW1zIFMuQS4xJDAiBgNVBAMT
# G0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQTAeFw0yNjAzMTEwNzM0NTRaFw0z
# NjAyMjcwNzM0NTRaMFAxCzAJBgNVBAYTAlBMMSEwHwYDVQQKDBhBc3NlY28gRGF0
# YSBTeXN0ZW1zIFMuQS4xHjAcBgNVBAMMFUNlcnR1bSBUaW1lc3RhbXAgMjAyNjCC
# AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALi9zfBw+xI12hi2F9uNQBNO
# STkTIi13fuh3DiWybI5392bBfJw6tO7zDs7UqP/rejVf4xFTU3XNOPsuOFkP5SPF
# snWwn3VJL+NhNhsuujvYsQtaQnvNo1wk46reTgws1L8OFEdKaxgyqT4zOQx81mLw
# NZSqXNU9Rrb9oAJWJF4Mydz+C1ebu0D0+vgM3tqAQ8TU203fqgKCPqIdHloaFqH9
# eyR+8tnpmy2hnnN59ZOtmsuHeRwf5iTOhlhf0d/qqquLOSIGPgGVL1Hj5XBUox2O
# E7tQRr+l/xxNbL6OuwBO4Aa2EbuxYg37cpXv5i2/lTL4NASB/duf9W4fjA3Ro+GP
# +bC54n2OYiM3Cq8z92ejx8F5HWO2e4/75eC95B9tMw/eq7109GDyW2+2ut5Bn0o8
# MXJTERkyystWbqcnUkomA97qfiDCwvpWuOMdfTOksXe9S+xDxOJaCbiyTivgPulx
# cT+tfQTRXBr5UwtPvc+INnafOEilzVHA11tv+pHcylryxBc08cUihf/ttqkZS0bm
# x3VxQay8uohaTp0//ECA9vYGW51SKdJMJhXvKyIdBLy8WYNchTt8yRITv9Adxg9J
# qGujh+TnP1bW3aQjFtlEl7SzgNeI8OVA/CJAWWUuRT3Au9ToOpHy/flrrknhN4he
# Ew7/6ILMv4HSsj0YZ4otAgMBAAGjggFQMIIBTDB1BggrBgEFBQcBAQRpMGcwOwYI
# KwYBBQUHMAKGL2h0dHA6Ly9zdWJjYS5yZXBvc2l0b3J5LmNlcnR1bS5wbC9jdHNj
# YTIwMjEuY2VyMCgGCCsGAQUFBzABhhxodHRwOi8vc3ViY2Eub2NzcC1jZXJ0dW0u
# Y29tMB8GA1UdIwQYMBaAFL5UAi+/QGxzQ86sCSVOnkNEGu7gMAwGA1UdEwEB/wQC
# MAAwOQYDVR0fBDIwMDAuoCygKoYoaHR0cDovL3N1YmNhLmNybC5jZXJ0dW0ucGwv
# Y3RzY2EyMDIxLmNybDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAOBgNVHQ8BAf8E
# BAMCB4AwIgYDVR0gBBswGTAIBgZngQwBBAIwDQYLKoRoAYb2dwIFAQswHQYDVR0O
# BBYEFCM5aiirmhKnpKfyH/lFmr+N/oIDMA0GCSqGSIb3DQEBDAUAA4ICAQBm/pOb
# rAveTqYFPCSthxNVwBHTW4TfuBQY2KGCdMlH6ALjVPYf2XARNGMTXScR0JQ2/c2L
# xLpoHHhRwxFf3d933DhFS165rYj6iPSQKm41rc9AMpTCjMdmai1aK9o870pOzTzL
# RhWFVIxm+5Qf+t4V5z14tPoUsAWdfRKD1Zzvzfe5AHlE9rxOZcGROiULxs3Edf7Z
# J8RZI0jzD9juj2oEX2XS7BVKuNCK1W0h1mjapv028yqo8UtZh5/1Ib48Yyp88BDY
# Shh5PSGUUuS5tYXVfcduIPBe3PdNTYRYdT5VNxkwLgjMzIcr/6SH2fejFJDjnCxj
# vXLuarErJg/IdI+hXdGIBDVOGv1zrkF4ktoQvFZIp5DIAYh0GwyUhc87qqn5UaYT
# j2+iyiBDSNh7aGCrkuAnqg7Qh7frhX+QhMGeJ9SpLpnG76qN85hLyjvYR6gRYiLT
# j8G4fxjqV400/buPw0rBo9yJZyq/8cGdCOLPOYf9qpefTcrZKeO9pg+UJHx8UABb
# aTqizRxm8sUSEycccCCFhIit3AI/3wGuSOADeiA676NcQ7+dsP04YvzvdN7pTZNy
# g672WpBDJdg0eItHl4yomvNeIOWh1qs4QqOW6TI8Ocsi4uSDTLorxLUDnBTwKQzz
# pxd1YUctEgRh32R0J9FkFkeEruJU94RljFr1uDCCBrkwggShoAMCAQICEQCZo4AK
# JlU7ZavcboSms+o5MA0GCSqGSIb3DQEBDAUAMIGAMQswCQYDVQQGEwJQTDEiMCAG
# A1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVt
# IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3Rl
# ZCBOZXR3b3JrIENBIDIwHhcNMjEwNTE5MDUzMjE4WhcNMzYwNTE4MDUzMjE4WjBW
# MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEu
# MSQwIgYDVQQDExtDZXJ0dW0gQ29kZSBTaWduaW5nIDIwMjEgQ0EwggIiMA0GCSqG
# SIb3DQEBAQUAA4ICDwAwggIKAoICAQCdI88EMCM7wUYs5zNzPmNdenW6vlxNur3r
# Lfi+5OZ+U3iZIB+AspO+CC/bj+taJUbMbFP1gQBJUzDUCPx7BNLgid1TyztVLn52
# NKgxxu8gpyTr6EjWyGzKU/gnIu+bHAse1LCitX3CaOE13rbuHbtrxF2tPU8f253Q
# gX6eO8yTbGps1Mg+yda3DcTsOYOhSYNCJiL+5wnjZ9weoGRtvFgMHtJg6i671OPX
# IciiHO4Lwo2p9xh/tnj+JmCQEn5QU0NxzrOiRna4kjFaA9ZcwSaG7WAxeC/xoZSx
# F1oK1UPZtKVt+yrsGKqWONoK6f5EmBOAVEK2y4ATDSkb34UD7JA32f+Rm0wsr5aj
# zftDhA5mBipVZDjHpwzv8bTKzCDUSUuUmPo1govD0RwFcTtMXcfJtm1i+P2UNXad
# PyYVKRxKQATHN3imsfBiNRdN5kiVVeqP55piqgxOkyt+HkwIA4gbmSc3hD8ke66t
# 9MjlcNg73rZZlrLHsAIV/nJ0mmgSjBI/TthoGJDydekOQ2tQD2Dup/+sKQptalDl
# ui59SerVSJg8gAeV7N/ia4mrGoiez+SqV3olVfxyLFt3o/OQOnBmjhKUANoKLYlK
# mUpKEFI0PfoT8Q1W/y6s9LTI6ekbi0igEbFUIBE8KDUGfIwnisEkBw5KcBZ3XwnH
# mfznwlKo8QIDAQABo4IBVTCCAVEwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU
# 3XRdTADbe5+gdMqxbvc8wDLAcM0wHwYDVR0jBBgwFoAUtqFUOQLDoD+Oirz61Pgc
# ptE6Dv0wDgYDVR0PAQH/BAQDAgEGMBMGA1UdJQQMMAoGCCsGAQUFBwMDMDAGA1Ud
# HwQpMCcwJaAjoCGGH2h0dHA6Ly9jcmwuY2VydHVtLnBsL2N0bmNhMi5jcmwwbAYI
# KwYBBQUHAQEEYDBeMCgGCCsGAQUFBzABhhxodHRwOi8vc3ViY2Eub2NzcC1jZXJ0
# dW0uY29tMDIGCCsGAQUFBzAChiZodHRwOi8vcmVwb3NpdG9yeS5jZXJ0dW0ucGwv
# Y3RuY2EyLmNlcjA5BgNVHSAEMjAwMC4GBFUdIAAwJjAkBggrBgEFBQcCARYYaHR0
# cDovL3d3dy5jZXJ0dW0ucGwvQ1BTMA0GCSqGSIb3DQEBDAUAA4ICAQB1iFgP5Y9Q
# KJpTnxDsQ/z0O23JmoZifZdEOEmQvo/79PQg9nLF/GJe6ZiUBEyDBHMtFRK0mXj3
# Qv3gL0sYXe+PPMfwmreJHvgFGWQ7XwnfMh2YIpBrkvJnjwh8gIlNlUl4KENTK5DL
# qsYPEtRQCw7R6p4s2EtWyDDr/M58iY2UBEqfUU/ujR9NuPyKk0bEcEi62JGxauFY
# zZ/yld13fHaZskIoq2XazjaD0pQkcQiIueL0HKiohS6XgZuUtCKA7S6CHttZEsOb
# QJ1j2s0urIDdqF7xaXFVaTHKtAuMfwi0jXtF3JJphrJfc+FFILgCbX/uYBPBlbBI
# P4Ht4xxk2GmfzMn7oxPITpigQFJFWuzTMUUgdRHTxaTSKRJ/6Uh7ki/pFjf9sUAS
# WgxT69QF9Ki4JF5nBIujxZ2sOU9e1HSCJwOfK07t5nnzbs1LbHuAIGJsRJiQ6HX/
# DW1XFOlXY1rc9HufFhWU+7Uk+hFkJsfzqBz3pRO+5aI6u5abI4Qws4YaeJH7H7M8
# X/YNoaArZbV4Ql+jarKsE0+8XvC4DJB+IVcvC9Ydqahi09mjQse4fxfef0L7E3hh
# o2O3bLDM6v60rIRUCi2fJT2/IRU5ohgyTch4GuYWefSBsp5NPJh4QRTP9DC3gc5Q
# EKtbrTY0Ka87Web7/zScvLmvQBm8JDFpDjCCBrkwggShoAMCAQICEQDn/2nHOzXO
# S5Em2HR8aKWHMA0GCSqGSIb3DQEBDAUAMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UE
# ChMZVW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENl
# cnRpZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBO
# ZXR3b3JrIENBIDIwHhcNMjEwNTE5MDUzMjA3WhcNMzYwNTE4MDUzMjA3WjBWMQsw
# CQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQw
# IgYDVQQDExtDZXJ0dW0gVGltZXN0YW1waW5nIDIwMjEgQ0EwggIiMA0GCSqGSIb3
# DQEBAQUAA4ICDwAwggIKAoICAQDpEh8ENe25XXrFppVBvoplf0530W0lddNmjtv4
# YSh/f7eDQKFaIqc7tHj7ox+u8vIsJZlroakUeMS3i3T8aJRC+eQs4FF0GqvkM6+W
# ZO8kmzZfxmZaBYmMLs8FktgFYCzywmXeQ1fEExflee2OpbHVk665eXRHjH7MYZIz
# Nnjl2m8Hy8ulB9mR8wL/W0v0pjKNT6G0sfrx1kk+3OGosFUb7yWNnVkWKU4qSxLv
# 16kJ6oVJ4BSbZ4xMak6JLeB8szrK9vwGDpvGDnKCUMYL3NuviwH1x4gZG0JAXU3x
# 2pOAz91JWKJSAmRy/l0s0l5bEYKolg+DMqVhlOANd8Yh5mkQWaMEvBRE/kAGzIqg
# WhwzN2OsKIVtO8mf5sPWSrvyplSABAYa13rMYnzwfg08nljZHghquCJYCa/xHK9a
# cev9UD7Y+usr15d7mrszzxhF1JOr1Mpup2chNSBlyOObhlSO16rwrffVrg/SzaKf
# SndS5swRhr8bnDqNJY9TNyEYvBYpgF95K7p0g4LguR4A++Z1nFIHWVY5v0fNVZmg
# zxD9uVo/gta3onGOQj3JCxgYx0KrCXu4yc9QiVwTFLWbNdHFSjBCt5/8Q9pLuRhV
# ocdCunhcHudMS1CGQ/Rn0+7P+fzMgWdRKfEOh/hjLrnQ8BdJiYrZNxvIOhM2aa3z
# EDHNwwIDAQABo4IBVTCCAVEwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUvlQC
# L79AbHNDzqwJJU6eQ0Qa7uAwHwYDVR0jBBgwFoAUtqFUOQLDoD+Oirz61PgcptE6
# Dv0wDgYDVR0PAQH/BAQDAgEGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMDAGA1UdHwQp
# MCcwJaAjoCGGH2h0dHA6Ly9jcmwuY2VydHVtLnBsL2N0bmNhMi5jcmwwbAYIKwYB
# BQUHAQEEYDBeMCgGCCsGAQUFBzABhhxodHRwOi8vc3ViY2Eub2NzcC1jZXJ0dW0u
# Y29tMDIGCCsGAQUFBzAChiZodHRwOi8vcmVwb3NpdG9yeS5jZXJ0dW0ucGwvY3Ru
# Y2EyLmNlcjA5BgNVHSAEMjAwMC4GBFUdIAAwJjAkBggrBgEFBQcCARYYaHR0cDov
# L3d3dy5jZXJ0dW0ucGwvQ1BTMA0GCSqGSIb3DQEBDAUAA4ICAQC4k1l3yUwV/ZQH
# CKCneqAs8EGTnwEUJLdDpokN/dMhKjK0rR5qX8nIIHzxpQR3TAw2IRw1Uxsr2Pli
# G3bCFqSdQTUbfaTq6V3vBzEebDru9QFjqlKnxCF2h1jhLNFFplbPJiW+JSnJTh1f
# KEqEdKdxgl9rVTvlxfEJ7exOn25MGbd/wGPwuSmMxRJVO0wnqgS7kmoJjNF9zqee
# hFSDDP8ZVkWg4EZ2tIS0M3uZmByRr+1Lkwjjt8AtW83mVnZTyTsOb+FNfwJY7DS4
# FmWhkRbgcHRetreoTirPOr/ozyDKhT8MTSTf6Lttg6s6T/u08mDWw6HK04ZRDfQ9
# sb77QV8mKgO44WGP31vXnVKoWVJpFBjPvjL8/Zck/5wXX2iqjOaLStFOR/IQki+E
# hn4zlcgVm22ZVCBPF+l8nAwUUShCtKuSU7GmZLKCmmxQMkSiWILTm8EtVD6AxnJh
# oq8EnhjEEyUoflkeRF2WhFiVQOmWTwZRr44IxWGkNJC6tTorW5rl2Zl+2e9JLPYf
# 3pStAPMDoPKIjVXd6NW2+fZrNUBeDo2eOa5Fn7Brs/HLQff5Xgris5MeUbdVgDrF
# 8uxO6cLPvZPo63j62SsNg55pTWk9fUIF9iPoRbb4QurjoY/woI1RAOKtYtTic6aA
# Jq3u83RIPpGXBSJKwx4KJAOZnCDCtTGCBqIwggaeAgEBMGowVjELMAkGA1UEBhMC
# UEwxITAfBgNVBAoTGEFzc2VjbyBEYXRhIFN5c3RlbXMgUy5BLjEkMCIGA1UEAxMb
# Q2VydHVtIENvZGUgU2lnbmluZyAyMDIxIENBAhBX5gfP8LabvnxCXoItEBa/MA0G
# CWCGSAFlAwQCAQUAoIGEMBgGCisGAQQBgjcCAQwxCjAIoAKAAKECgAAwGQYJKoZI
# hvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcC
# ARUwLwYJKoZIhvcNAQkEMSIEIOYdCvUoV/7AyBaljYbw4srsq4i98keb5EQf7B9c
# ALyDMA0GCSqGSIb3DQEBAQUABIIBgCrVsQvEguwq3/W2D26qkYINihJnaeVCE13D
# gDMQ3LvY5SqVzvAPnTGLcKVsWQXI3SFXtWhLPpTYdU1JeqaqasRQtMyb7CLc+zRV
# 2CM4TZFAllFdZrI0eIZPZtAnD77EtDtAOA9aNStioTx3eP050xzyaqZuvBXLyo95
# 6gGlyOyjiEQz+r6k+XRH/Tu7ooctKo0UJRd8ZhaUycN6Zc7bYA6IVYqD4Mtzoh2x
# rpiRAdU+KTJx8K9Gn7skAu5jQXmpo9I0mTNnndlTK4ctlsGjzT0aKMHNZpTQnTco
# +VQb3KRTBLkcfHqgbjKIenyE1oU+8aHtAP9JBEUcj6TvJ5mFKdyJ1QkBojSjeMn5
# egXAsWQdDG5XPIviL7csmcYiXiQxBQSxWUrPkEZZ/D5Hog5n+HP4mYvM2jmGm9kZ
# aZM+8W4kfIDUGJcOBizHpAgI09glq6HlwSFtmgov1VIaeI5R++H8iSyuBQHMUSBF
# KKT5PcVN2ZbrZgQXaL4+Q8ew+M/Fj6GCBAIwggP+BgkqhkiG9w0BCQYxggPvMIID
# 6wIBATBqMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0
# ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQQIQ
# KPB3wRw2vf5fdDJHcCcuAzANBglghkgBZQMEAgIFAKCCAVYwGgYJKoZIhvcNAQkD
# MQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNjEwMTMwOTQwMzlaMDcG
# CyqGSIb3DQEJEAIvMSgwJjAkMCIEIIW+kOEK0kONfMkotq9IsJqyCBd87PiwEmxY
# 05EFJcQ8MD8GCSqGSIb3DQEJBDEyBDBETYGB2UNqj/Q/2+cqcjcSIPO5ivUkyCPj
# WayMKgZSTU890EBpTnq0lXhO9Tso7p0wgZ8GCyqGSIb3DQEJEAIMMYGPMIGMMIGJ
# MIGGBBRXFGhBDKha80JO+RZKUTYQ9NONmDBuMFqkWDBWMQswCQYDVQQGEwJQTDEh
# MB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQwIgYDVQQDExtDZXJ0
# dW0gVGltZXN0YW1waW5nIDIwMjEgQ0ECECjwd8EcNr3+X3QyR3AnLgMwDQYJKoZI
# hvcNAQEBBQAEggIAaXJ8v5xz5nlSDaU0gw6dXFsmrXX/+IL9LuK2DQgPVCewXs5x
# EA2WeDSxVFYJ9HVEsdPU1ef70DcCTTpOEGeEHjo0HkJaFGC+EiWrGRFljCwmqrsl
# 2xAAvD8X6zEwrlAmbTg53eVOT7bsmf189tKdVG3lNXBWnSiNFdWWqpDrQDwWaaBC
# EF42dQVleqadUoy9Bkz/C8DzygPCfsNuHL2xyvvvOyY18j93kosL/g3G1IJpd2hj
# 5y2IKTHtXMRzG7+zWQJwQ3WdFILkh3nOsh5BRaNsXw9afCU+oF8U0NVX7b28MWys
# GQ5uuWYDCZVHr7Rqs/PZfZugyDkfrjlPHE6GnnV8N/MyEWMc+0nSV+p/6VxOQrlx
# ivGqmAXoa3GyoSjWqfbQ8CRn3YWorYgLsgYlJTpmsexx1Rn5OaSn55mHuq8DVhlD
# 2QRw+p887tLwrPO9T3ElMFywKYcQXQydWuVjCels1CmnrEkkPCQQlgtrNyj0bPYx
# 0iHNPK/9CNve+fSpKTkxAjd69hEDgnhbkR/bkow1zVyikeKE3G3Uo1eunszQVvoK
# RttoK79/g24Aa1XdTeydN09uQbV/XmhGuLYZt7N3GhJMe/yA6Vw2+EZg0d0+gbRk
# hfYwH7oFjoMNIE59zXNQnRgsizDqt2nK/2utuStkBIYkIJ6Z+mLCyMtPrgA=
# SIG # End signature block