Private/Invoke-EntraOpsParallelObjectResolution.ps1

<#
.SYNOPSIS
    Internal helper function for parallel object detail resolution across EntraOps functions.

.DESCRIPTION
    Provides reusable parallel processing logic for resolving object details using Microsoft Graph SDK's
    process-level authentication context. Falls back to sequential processing when parallel is not viable.

.PARAMETER UniqueObjects
    Array of unique objects with ObjectId and ObjectType properties to resolve.

.PARAMETER TenantId
    Tenant ID for object resolution.

.PARAMETER EnableParallelProcessing
    Enable parallel processing. Default is $true.

.PARAMETER ParallelThrottleLimit
    Maximum number of parallel threads. Default is 10.

.OUTPUTS
    Hashtable with ObjectId as key and object details as value.
#>


function Invoke-EntraOpsParallelObjectResolution {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [Array]$UniqueObjects,
        
        [Parameter(Mandatory = $true)]
        [string]$TenantId,
        
        [Parameter(Mandatory = $false)]
        [bool]$EnableParallelProcessing = $true,
        
        [Parameter(Mandatory = $false)]
        [int]$ParallelThrottleLimit = 10
    )
    
    $UniqueObjectIds = @($UniqueObjects.ObjectId)
    
    #region Pre-fetch objects by type for massive performance improvement (Recommendation 2)
    # Standardized cache population using batch requests
    Write-Host "Pre-fetching object details to populate cache..."
    $PreFetchStats = @{ PreFetchedCount = 0; FailedCount = 0 }
    
    # Use beta endpoint to match Get-EntraOpsPrivilegedEntraObject expectations
    # We fetch ALL properties required by the consumer function in the batch call
    $PropsToSelect = "id,displayName,userPrincipalName,userType,isAssignableToRole,isManagementRestricted,onPremisesSyncEnabled,passwordPolicies"
    
    # Reduce batch size from 1000 to 100 to prevent 504 Gateway Timeouts
    $BatchSize = 100
    $TotalBatches = [Math]::Ceiling($UniqueObjectIds.Count / $BatchSize)
    $CurrentBatch = 0
    
    try {
        # Split into smaller batches to avoid Gateway Timeout (504)
        for ($i = 0; $i -lt $UniqueObjectIds.Count; $i += $BatchSize) {
            $CurrentBatch++
            $Batch = $UniqueObjectIds[$i..([Math]::Min($i + $BatchSize - 1, $UniqueObjectIds.Count - 1))]
            
            # Show progress for large datasets
            if ($TotalBatches -gt 1) {
                $PercentComplete = [Math]::Round(($CurrentBatch / $TotalBatches) * 100, 0)
                Write-Progress -Activity "Pre-fetching Object Details" -Status "Batch $CurrentBatch of $TotalBatches ($($Batch.Count) objects)" -PercentComplete $PercentComplete -Id 50
            }
            
            $Body = @{
                ids   = $Batch
                types = @('user', 'group', 'servicePrincipal')
            } | ConvertTo-Json
            
            $Uri = "/beta/directoryObjects/getByIds?`$select=$PropsToSelect"
            
            try {
                $PreFetchedObjects = Invoke-EntraOpsMsGraphQuery -Method POST -Uri $Uri -Body $Body -OutputType PSObject
                
                # POPULATE CACHE: Key objects so Get-EntraOpsPrivilegedEntraObject finds them
                if ($PreFetchedObjects) {
                    $PreFetchedObjects = @($PreFetchedObjects) # Ensure array
                    $PreFetchStats.PreFetchedCount += $PreFetchedObjects.Count
                    
                    # Resolve the tenant discriminator exactly as Invoke-EntraOpsMsGraphQuery does,
                    # so pre-populated keys are found by its cache lookup (which is tenant-scoped
                    # to prevent cross-tenant cache poisoning when processing multiple tenants in
                    # the same session/runspace). Order must match: CurrentGraphTenantId (cross-tenant
                    # switch, REST-only) -> MgContext -> TenantIdContext -> default.
                    $CacheTenantId = $__EntraOpsSession['CurrentGraphTenantId']
                    if ([string]::IsNullOrEmpty($CacheTenantId)) {
                        try {
                            $CacheTenantId = (Get-MgContext -ErrorAction Stop).TenantId
                        } catch {
                            Write-Verbose "Unable to determine current Graph tenant context for cache scoping: $_"
                        }
                    }
                    if ([string]::IsNullOrEmpty($CacheTenantId)) {
                        $CacheTenantId = $Global:TenantIdContext
                    }
                    if ([string]::IsNullOrEmpty($CacheTenantId)) {
                        $CacheTenantId = "default"
                    }

                    foreach ($Obj in $PreFetchedObjects) {
                        # Construct the exact cache key the consumer function will use
                        # Must match the full URL + tenant format used by Invoke-EntraOpsMsGraphQuery
                        $CacheKey = "$CacheTenantId#https://graph.microsoft.com/beta/directoryObjects/$($Obj.id)?`$select=$PropsToSelect"
                        
                        if (-not $__EntraOpsSession.GraphCache.ContainsKey($CacheKey)) {
                            $__EntraOpsSession.GraphCache[$CacheKey] = $Obj
                            
                            # Add valid metadata to prevent expiration checks from failing
                            $__EntraOpsSession.CacheMetadata[$CacheKey] = @{
                                Uri         = $CacheKey
                                CachedTime  = [DateTime]::UtcNow
                                ExpiryTime  = [DateTime]::UtcNow.AddSeconds($__EntraOpsSession.DefaultCacheTTL)
                                ResultCount = 1
                            }
                        }
                    }
                }
            } catch {
                Write-Warning "Batch $CurrentBatch failed: $($_.Exception.Message)"
                $PreFetchStats.FailedCount += $Batch.Count
            }
            
            # Longer anti-throttle delay to prevent 504 errors (increased from 200ms to 500ms)
            if ($i + $BatchSize -lt $UniqueObjectIds.Count) { 
                Start-Sleep -Milliseconds 500 
            }
        }
        
        if ($TotalBatches -gt 1) {
            Write-Progress -Activity "Pre-fetching Object Details" -Completed -Id 50
        }
    } catch {
        Write-Warning "Pre-fetch failed: $($_.Exception.Message)"
        $PreFetchStats.FailedCount += $UniqueObjectIds.Count
    }
    
    Write-Host "Pre-fetch complete: $($PreFetchStats.PreFetchedCount) objects cached, $($PreFetchStats.FailedCount) failed."
    #endregion
    
    Write-Host "Resolving details for $($UniqueObjectIds.Count) unique objects..."
    $ObjectDetailsCache = @{}
    
    # Determine if parallel processing is viable (PowerShell 7+ guaranteed by module prerequisite)
    # Parallel runspaces authenticate either through the Graph SDK's process-wide auth context
    # (SDK mode) or through the shared session's pre-warmed MsGraphTokenCache (REST-only mode:
    # Connect-EntraOps -UseInvokeRestMethodOnly skips Connect-MgGraph entirely for non-interactive
    # auth, so no SDK context exists - the token is pre-warmed here in the main thread because
    # Get-AzAccessToken inside fresh runspaces is not guaranteed to see the Az context).
    # Threshold 5: sequential resolution costs ~4-6s per object (Get-EntraOpsPrivilegedEntraObject
    # makes several Graph calls beyond the pre-fetch), so the runspace pool pays off well below the
    # former threshold of 20.
    $HasSufficientObjects = $UniqueObjectIds.Count -ge 5
    $IsRestOnlyMode = [bool]$__EntraOpsSession['UseInvokeRestMethodOnly']
    $IsUsingMgGraphSDK = $false
    if (-not $IsRestOnlyMode) {
        try {
            $IsUsingMgGraphSDK = $null -ne (Get-MgContext -ErrorAction Stop)
        } catch {
            Write-Verbose "Microsoft Graph SDK context not available - parallel object resolution requires it: $($_.Exception.Message)"
        }
    }
    $RestOnlyParallelReady = $false
    if ($IsRestOnlyMode -and $EnableParallelProcessing) {
        $RestOnlyParallelReady = Initialize-EntraOpsRestOnlyParallelToken
    }
    $HasParallelAuth = $IsUsingMgGraphSDK -or $RestOnlyParallelReady
    $UseParallel = $EnableParallelProcessing -and $HasSufficientObjects -and $HasParallelAuth
    
    if ($UseParallel) {
        $AuthModeLabel = if ($IsUsingMgGraphSDK) { "Microsoft Graph SDK authentication" } else { "REST-only with pre-warmed token" }
        Write-Host "Using parallel processing with $ParallelThrottleLimit threads ($AuthModeLabel)..."
        
        # SDK mode: verify MgGraph connection exists (REST-only mode was validated by the pre-warm)
        $MgGraphModulePath = $null
        if ($IsUsingMgGraphSDK) {
            $MgContext = Get-MgContext
            if ($null -eq $MgContext) {
                Write-Warning "Microsoft Graph is not connected. Falling back to sequential processing."
                $UseParallel = $false
            } else {
                Write-Verbose "MgGraph Context: TenantId=$($MgContext.TenantId), Scopes=$($MgContext.Scopes -join ', ')"
                Import-Module Microsoft.Graph.Authentication -ErrorAction SilentlyContinue
                $MgGraphModulePath = (Get-Module Microsoft.Graph.Authentication).Path
            }
        }
        
        if ($UseParallel) {
            $EntraOpsModulePath = Split-Path $PSScriptRoot -Parent
            
            # Capture global variables needed by Get-EntraOpsPrivilegedEntraObject
            $LocalEntraOpsConfig = $Global:EntraOpsConfig
            $LocalEntraOpsBaseFolder = $Global:EntraOpsBaseFolder
            $LocalTenantIdContext = $Global:TenantIdContext
            $LocalEntraOpsSession = $Script:__EntraOpsSession

            try {
                $ParallelResults = $UniqueObjects | ForEach-Object -ThrottleLimit $ParallelThrottleLimit -Parallel {
                    $obj = $_
                    $ObjectId = $obj.ObjectId
                    $LocalTenantId = $using:TenantId
                    $LocalEntraOpsPath = $using:EntraOpsModulePath
                    $LocalMgGraphPath = $using:MgGraphModulePath
                    $LocalIsRestOnly = $using:IsRestOnlyMode
                    
                    try {
                        if (-not $LocalIsRestOnly) {
                            if (-not (Get-Module -Name Microsoft.Graph.Authentication)) {
                                Import-Module $LocalMgGraphPath -ErrorAction Stop
                            }
                            
                            $ThreadMgContext = Get-MgContext
                            if ($null -eq $ThreadMgContext) {
                                throw "MgGraph context not available in parallel runspace"
                            }
                        }
                        
                        # Import EntraOps module to get all functions
                        $EntraOpsModuleManifest = Join-Path $LocalEntraOpsPath "EntraOps.psd1"
                        if (Test-Path $EntraOpsModuleManifest) {
                            $env:ENTRAOPS_NOWELCOME = $true
                            if (-not (Get-Module -Name EntraOps)) {
                                Import-Module $EntraOpsModuleManifest -ErrorAction Stop -WarningAction SilentlyContinue
                            }
                        } else {
                            throw "EntraOps module manifest not found at $EntraOpsModuleManifest"
                        }
                        
                        # Inject the shared session INTO THE MODULE SCOPE: module functions resolve
                        # $__EntraOpsSession from their own script scope (created fresh on import), so a
                        # runspace-local $script:/$global: assignment never reaches them. Required for
                        # REST-only auth (pre-warmed MsGraphTokenCache + UseInvokeRestMethodOnly flag)
                        # and makes the pre-fetched GraphCache visible to runspaces in both modes.
                        if ($using:LocalEntraOpsSession) {
                            $script:__EntraOpsSession = $using:LocalEntraOpsSession
                            $EntraOpsModuleInstance = Get-Module -Name EntraOps
                            & $EntraOpsModuleInstance { param($s) Set-Variable -Name __EntraOpsSession -Value $s -Scope Script } $script:__EntraOpsSession
                        } elseif (-not (Get-Variable -Name __EntraOpsSession -Scope Script -ErrorAction SilentlyContinue)) {
                            $script:__EntraOpsSession = @{
                                GraphCache    = @{}
                                CacheMetadata = @{}
                            }
                        }
                        
                        # Restore global variables in parallel runspace. UseInvokeRestMethodOnly mirrors the
                        # session mode: SDK mode authenticates via the process-wide MgGraph context, REST-only
                        # mode via the shared session's pre-warmed token cache
                        New-Variable -Name UseInvokeRestMethodOnly -Value $LocalIsRestOnly -Scope Global -Force -ErrorAction SilentlyContinue
                        New-Variable -Name EntraOpsConfig -Value $using:LocalEntraOpsConfig -Scope Global -Force -ErrorAction SilentlyContinue
                        New-Variable -Name EntraOpsBaseFolder -Value $using:LocalEntraOpsBaseFolder -Scope Global -Force -ErrorAction SilentlyContinue
                        # Cache-key tenant scoping in REST-only mode falls back to this global (no MgContext)
                        New-Variable -Name TenantIdContext -Value $using:LocalTenantIdContext -Scope Global -Force -ErrorAction SilentlyContinue
                        
                        $ObjectDetails = Get-EntraOpsPrivilegedEntraObject -AadObjectId $ObjectId -TenantId $LocalTenantId
                        
                        [PSCustomObject]@{
                            ObjectId = $ObjectId
                            Details  = $ObjectDetails
                            Success  = $true
                            ThreadId = [System.Threading.Thread]::CurrentThread.ManagedThreadId
                        }
                    } catch {
                        [PSCustomObject]@{
                            ObjectId = $ObjectId
                            Details  = $null
                            Success  = $false
                            Error    = $_.Exception.Message
                            ThreadId = [System.Threading.Thread]::CurrentThread.ManagedThreadId
                        }
                    }
                }
                
                # Process results
                $SuccessCount = 0
                $FailureCount = 0
                $UsedThreads = @()
                
                foreach ($Result in $ParallelResults) {
                    if ($Result.Success) {
                        $ObjectDetailsCache[$Result.ObjectId] = $Result.Details
                        $SuccessCount++
                        if ($Result.ThreadId -notin $UsedThreads) { $UsedThreads += $Result.ThreadId }
                    } else {
                        Write-Warning "Failed to get details for object $($Result.ObjectId): $($Result.Error)"
                        $ObjectDetailsCache[$Result.ObjectId] = $null
                        $FailureCount++
                    }
                }
                
                Write-Host "Parallel processing completed: $SuccessCount successful, $FailureCount failed (used $($UsedThreads.Count) threads)"
                
            } catch {
                Write-Warning "Parallel processing failed: $($_.Exception.Message). Falling back to sequential processing."
                $UseParallel = $false
            }
        }
    }
    
    # Sequential processing fallback
    if (-not $UseParallel) {
        if ($EnableParallelProcessing) {
            $Reasons = @()
            if (-not $HasSufficientObjects) { $Reasons += "dataset too small (<5 objects)" }
            if (-not $HasParallelAuth) {
                $Reasons += if ($IsRestOnlyMode) { "Graph token pre-warm for REST-only parallel processing failed" } else { "Microsoft Graph SDK context not available" }
            }
            if ($Reasons.Count -gt 0) {
                Write-Host "Using sequential processing: $($Reasons -join ', ')"
            }
        } else {
            Write-Host "Using sequential processing (parallel disabled)..."
        }
        
        for ($i = 0; $i -lt $UniqueObjectIds.Count; $i++) {
            $ObjectId = $UniqueObjectIds[$i]
            
            $ProgressInterval = [Math]::Max(10, [Math]::Floor($UniqueObjectIds.Count / 20))
            if (($i % $ProgressInterval) -eq 0 -or $i -eq ($UniqueObjectIds.Count - 1)) {
                $PercentComplete = [math]::Round(($i / $UniqueObjectIds.Count) * 100, 0)
                Write-Progress -Activity "Resolving Object Details" -Status "Processing object $($i + 1) of $($UniqueObjectIds.Count)" -PercentComplete $PercentComplete
                if ($VerbosePreference -ne 'SilentlyContinue') {
                    Write-Verbose "Processing object $($i + 1) of $($UniqueObjectIds.Count)..."
                }
            }
            
            try {
                $ObjectDetailsCache[$ObjectId] = Get-EntraOpsPrivilegedEntraObject -AadObjectId $ObjectId -TenantId $TenantId
            } catch {
                Write-Warning "Failed to get details for object $($ObjectId): $_"
                $ObjectDetailsCache[$ObjectId] = $null
            }
        }
        Write-Progress -Activity "Resolving Object Details" -Completed
    }
    
    return $ObjectDetailsCache
}

# SIG # Begin signature block
# MIIniAYJKoZIhvcNAQcCoIIneTCCJ3UCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDMmjWZO+7kVZsP
# m/FeQjpC61VYdrxq++169aTQKfeTA6CCIDwwggXJMIIEsaADAgECAhAbtY8lKt8j
# 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
# ARUwLwYJKoZIhvcNAQkEMSIEILGjmbK1lKu+Wu5VDPXnW1zV2859YY7pIY77yRrL
# NKXlMA0GCSqGSIb3DQEBAQUABIIBgCRMF425jyY4k0G0e8gQhTN4xSIR7bCtSiHt
# 0sGT1pxUyvcSBKLK58ic0l17wM4kLD9D9MHi700dcte5A7p5TVnxUj88C5pxR4EE
# gL4oYZa3lVyuUVFHxqc4dEuqxE8iZDVbtR4JdO2+CiTtwfpSFvqmfPi7KQik53yM
# jF4CXXe6meKmYNfAyLylAtVeoPq9r+zEVwp8pGTrFg+RJ1Unig6wa7Pgd9+uwjE1
# 3z+0VoELO3LFmhzW7Rm4woT7QhkzNmO3TeMCTln3Bu9Wtolbvd/NqGejQXGlctzN
# dyybmb5EenRDo95vjh5gfWIUtwB7t9aZ3RW8rrPN1nYxxO1qQUebcXZ2w3gDkNQJ
# jlV4VSr//QjmL7LsVRKQjLQGWxwAabg6DtWOF1TcuIdn7J0lbo6cT7WScoj1hbrz
# ST8cMTk+yTWh3Wa1tPJxSsOCuoEwCLckRLSrAcAvsJtOIMlYy7Gx7PzRh5LN1HQo
# 9vaio9V7LZniYTYLwRtUI6eqmJDPZaGCBAIwggP+BgkqhkiG9w0BCQYxggPvMIID
# 6wIBATBqMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0
# ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQQIQ
# KPB3wRw2vf5fdDJHcCcuAzANBglghkgBZQMEAgIFAKCCAVYwGgYJKoZIhvcNAQkD
# MQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNjEwMTMwOTQwMDNaMDcG
# CyqGSIb3DQEJEAIvMSgwJjAkMCIEIIW+kOEK0kONfMkotq9IsJqyCBd87PiwEmxY
# 05EFJcQ8MD8GCSqGSIb3DQEJBDEyBDDYL+IO18q4dSCt8S0fONcZ+zg52fby5YPi
# 98AxWQML4D7thSAINWHzmKlOXKmPYw8wgZ8GCyqGSIb3DQEJEAIMMYGPMIGMMIGJ
# MIGGBBRXFGhBDKha80JO+RZKUTYQ9NONmDBuMFqkWDBWMQswCQYDVQQGEwJQTDEh
# MB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQwIgYDVQQDExtDZXJ0
# dW0gVGltZXN0YW1waW5nIDIwMjEgQ0ECECjwd8EcNr3+X3QyR3AnLgMwDQYJKoZI
# hvcNAQEBBQAEggIAH6I+odMhZYPyOM11I5iSgyqJBnnMj33YxnJMUlr0Xq2Zs28l
# lv1q30KzcT99GmWvGfWgE34ede9uY7IdxkVaKTpKPJk3ulrYuW6PiskqD05s2Sqo
# PUx9FUJN/kgu7KA+lGOh6aT522jL0ysyVMYlImVGrYR/wJ5RzQZgHQxxMP/YUcpT
# ThQfgDgJGHC6Rhu5yTSE+mnQ7hYXfs7VgRGSEIgt5rCYnfIJMN086zNDwB2IJU6b
# D9rG1UU0HEYq081Rag8o3/ZbDnExwDCjS1xG8Lpt/2Xk/eApjYYZONtRM840L/O/
# r6wxLKUR6LIGdpVfjv/whObq1U0sYULYDrq48NAg08EoNOW9z0heOy9mgXXtdvIq
# ozAOZYjuFwoSD21U/bEhD4ZSeEfE+ZXglfMQF9hgRSvK5EueILPCaZOy//9JOnBs
# 4GZK9mRlZzd2p8bycivpiuemasW05SV7B3BsJ+wMgHjEhP7vH5gh7hr/Y2Uzg77L
# iwBsxhwvzAQLs6N9tCU4oYlyrUayOtkzH57icBMl3ZXaPgzRlBebqit+vMsVqU57
# lE3kRL+eYKFaJ9wdst/iHiPJocJJK73hzAK252TvRhhuMKkON7cEVMi8ejMbC9K0
# 5QgzIcgu2Lo25I9TqF/6wnIPnj9E8zn2j8MDTGP2n3nJHAwK+0vLV8gJNoo=
# SIG # End signature block