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 |