Private/Invoke-EntraOpsEAMClassificationAggregation.ps1
|
<# .SYNOPSIS Aggregates classifications per unique object and builds standardized EAM output objects. .DESCRIPTION Shared helper that replaces the duplicated classification aggregation logic found in every EAM cmdlet's final stage. Handles both parallel and sequential processing paths, builds unique classification sets per object, applies "Unclassified" fallback, and constructs the standardized output PSCustomObject via New-EntraOpsEAMOutputObject. Principals whose object details could not be resolved ($null entry in ObjectDetailsCache) are NOT dropped: they are emitted fail-closed as ControlPlane placeholders with a Classification marker entry (TaggedBy = "UnresolvedObject"), so downstream Administrative Unit / Conditional Access group reconciliation never removes a potentially privileged principal from its protection group just because a Graph lookup failed or was throttled. This function eliminates ~150 lines of duplicated code per EAM cmdlet. .PARAMETER UniqueObjects Array of unique objects with ObjectId and ObjectType properties. .PARAMETER ObjectDetailsCache Hashtable mapping ObjectId → resolved object details from Get-EntraOpsPrivilegedEntraObject. .PARAMETER RbacClassificationsByObject Hashtable mapping ObjectId → classified role assignments (from Group-Object ObjectId -AsHashTable). .PARAMETER RoleSystem The RBAC system name (e.g., "EntraID", "Defender", "DeviceManagement", "IdentityGovernance", "ResourceApps"). .PARAMETER EnableParallelProcessing Enable parallel processing. Default is $true. .PARAMETER ParallelThrottleLimit Maximum number of parallel threads. Default is 10. .PARAMETER WarningMessages Reference to the List[psobject] for collecting warnings. .OUTPUTS Array of standardized EAM output objects. #> function Invoke-EntraOpsEAMClassificationAggregation { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [Array]$UniqueObjects, [Parameter(Mandatory = $true)] [hashtable]$ObjectDetailsCache, [Parameter(Mandatory = $true)] [hashtable]$RbacClassificationsByObject, [Parameter(Mandatory = $true)] [string]$RoleSystem, [Parameter(Mandatory = $false)] [bool]$EnableParallelProcessing = $true, [Parameter(Mandatory = $false)] [int]$ParallelThrottleLimit = 10, [Parameter(Mandatory = $false)] [System.Collections.Generic.List[psobject]]$WarningMessages ) # Determine if parallel processing is viable (PowerShell 7+ guaranteed by module prerequisite) $HasSufficientObjects = $UniqueObjects.Count -ge 50 $UseParallelForClassification = $EnableParallelProcessing -and $HasSufficientObjects # Fail-closed placeholder SCHEMA, defined once and shared by the parallel and sequential paths # so the two can never drift apart. Shared as plain data (read-only template + marker) rather # than a scriptblock: invoking a $using: scriptblock concurrently across parallel runspaces is # not thread-safe, and [scriptblock]::Create is avoided for the Constrained Language Mode goals # tracked in PawCompatible.md. Each path clones the template and fills in the three # object-specific fields (ObjectTenantId, ObjectType, ObjectDisplayName). $UnresolvedObjectDetailsTemplate = [ordered]@{ 'ObjectTenantId' = $null 'ObjectType' = 'unresolved' 'ObjectSubType' = 'Unresolved' 'ObjectDisplayName' = $null 'ObjectSignInName' = $null 'AdminTierLevel' = '0' 'AdminTierLevelName' = 'ControlPlane' 'OnPremSynchronized' = $null 'AssignedAdministrativeUnits' = $null 'RestrictedManagementByRAG' = $null 'RestrictedManagementByAadRole' = $null 'RestrictedManagementByRMAU' = $null 'Sponsors' = $null 'Owners' = $null 'OwnedObjects' = $null 'OwnedDevices' = $null 'IdentityParent' = $null 'AssociatedWorkAccount' = $null 'AssociatedPawDevice' = $null } $UnresolvedMarkerClassification = [PSCustomObject]@{ 'AdminTierLevel' = "0" 'AdminTierLevelName' = "ControlPlane" 'Service' = "Unresolved" 'TaggedBy' = "UnresolvedObject" } if ($UseParallelForClassification) { $SyncObjectDetailsCache = [System.Collections.Hashtable]::Synchronized($ObjectDetailsCache) $SyncRbacByObject = [System.Collections.Hashtable]::Synchronized($RbacClassificationsByObject) $ClassificationThrottleLimit = [Math]::Min($ParallelThrottleLimit * 2, 100) Write-Host "Using parallel classification processing with $ClassificationThrottleLimit threads for $($UniqueObjects.Count) objects..." -ForegroundColor Yellow $ClassifiedObjects = $UniqueObjects | ForEach-Object -ThrottleLimit $ClassificationThrottleLimit -Parallel { $obj = $_ $ObjectId = $obj.ObjectId if ($null -ne $ObjectId) { $SharedDetailsCache = $using:SyncObjectDetailsCache $SharedRbacByObject = $using:SyncRbacByObject $LocalRoleSystem = $using:RoleSystem $ObjectDetails = $SharedDetailsCache[$ObjectId] # Fail-closed: never drop a principal whose object details could not be resolved # (failed/throttled Graph call, $null cache entry). A vanished ControlPlane principal # would otherwise be removed from its protection group by the Administrative Unit / # Conditional Access group reconciliation. Emit a placeholder pinned to ControlPlane # instead, built inline from the identity fields known from the assignment data # (cannot call module functions from parallel runspace). $IsUnresolvedObject = ($null -eq $ObjectDetails) if ($IsUnresolvedObject) { # Shared schema template (see its definition in function scope) - clone and # fill the object-specific fields. $DetailsTemplate = $using:UnresolvedObjectDetailsTemplate $Details = [ordered]@{} foreach ($TemplateKey in $DetailsTemplate.Keys) { $Details[$TemplateKey] = $DetailsTemplate[$TemplateKey] } $Details['ObjectTenantId'] = $obj.ObjectTenantId $KnownObjectType = "$($obj.ObjectType)" if (-not [string]::IsNullOrWhiteSpace($KnownObjectType)) { $Details['ObjectType'] = $KnownObjectType } $Details['ObjectDisplayName'] = $ObjectId $ObjectDetails = [PSCustomObject]$Details } $RbacClassifiedAssignments = $SharedRbacByObject[$ObjectId] # Aggregate unique classifications using hashtable $UniqueClassificationsHash = @{} foreach ($Assignment in $RbacClassifiedAssignments) { if ($null -ne $Assignment.Classification) { foreach ($ClassItem in $Assignment.Classification) { $key = "$($ClassItem.AdminTierLevel)|$($ClassItem.AdminTierLevelName)|$($ClassItem.Service)" if (-not $UniqueClassificationsHash.ContainsKey($key)) { $UniqueClassificationsHash[$key] = $ClassItem } } } } $Classification = @($UniqueClassificationsHash.Values | Select-Object -Unique -ExcludeProperty TaggedBy, TaggedByObjectIds, TaggedByObjectDisplayNames, TaggedByRoleSystem | Sort-Object AdminTierLevel, AdminTierLevelName, Service) if ($Classification.Count -eq 0) { $Classification = @([PSCustomObject]@{ 'AdminTierLevel' = "Unclassified" 'AdminTierLevelName' = "Unclassified" 'Service' = "Unclassified" }) } if ($IsUnresolvedObject) { # Marker entry flags the fail-closed placeholder in the export and also serves as # the structured warning marker folded into $WarningMessages after the parallel # block. Keep any classifications derived from the assignments themselves, but # drop the generic "Unclassified" fallback in favor of the explicit marker. $Classification = @(($using:UnresolvedMarkerClassification).PSObject.Copy()) + @($Classification | Where-Object { $_.AdminTierLevelName -ne "Unclassified" }) } # Build output object inline (cannot call module functions from parallel runspace) [PSCustomObject]@{ 'ObjectId' = $ObjectId 'ObjectTenantId' = $ObjectDetails.ObjectTenantId 'ObjectType' = ($ObjectDetails.ObjectType ?? 'unknown').ToLower() 'ObjectSubType' = $ObjectDetails.ObjectSubType 'ObjectDisplayName' = $ObjectDetails.ObjectDisplayName 'ObjectUserPrincipalName' = $ObjectDetails.ObjectSignInName 'ObjectAdminTierLevel' = $ObjectDetails.AdminTierLevel 'ObjectAdminTierLevelName' = $ObjectDetails.AdminTierLevelName 'OnPremSynchronized' = $ObjectDetails.OnPremSynchronized 'AssignedAdministrativeUnits' = $ObjectDetails.AssignedAdministrativeUnits 'RestrictedManagementByRAG' = $ObjectDetails.RestrictedManagementByRAG 'RestrictedManagementByAadRole' = $ObjectDetails.RestrictedManagementByAadRole 'RestrictedManagementByRMAU' = $ObjectDetails.RestrictedManagementByRMAU 'RoleSystem' = $LocalRoleSystem 'Classification' = $Classification 'RoleAssignments' = @($RbacClassifiedAssignments | Sort-Object { ($_.Classification | Sort-Object AdminTierLevel | Select-Object -First 1).AdminTierLevel }, RoleDefinitionName, RoleAssignmentScopeId) 'Sponsors' = $ObjectDetails.Sponsors 'Owners' = $ObjectDetails.Owners 'OwnedObjects' = $ObjectDetails.OwnedObjects 'OwnedDevices' = $ObjectDetails.OwnedDevices 'IdentityParent' = $ObjectDetails.IdentityParent 'AssociatedWorkAccount' = $ObjectDetails.AssociatedWorkAccount 'AssociatedPawDevice' = $ObjectDetails.AssociatedPawDevice } } } # Fold unresolved-object markers from the parallel results into the warning collection: # parallel runspaces cannot append to $WarningMessages directly, so each placeholder carries # its marker (Classification TaggedBy = "UnresolvedObject") and is surfaced here. $UnresolvedClassifiedObjects = @($ClassifiedObjects | Where-Object { @($_.Classification).TaggedBy -contains "UnresolvedObject" }) foreach ($UnresolvedObject in $UnresolvedClassifiedObjects) { $UnresolvedWarning = "Object $($UnresolvedObject.ObjectId) could not be resolved - emitted as fail-closed ControlPlane placeholder (TaggedBy=UnresolvedObject)" if ($null -ne $WarningMessages) { $WarningMessages.Add([PSCustomObject]@{ Type = "UnresolvedObject" Message = $UnresolvedWarning Target = $UnresolvedObject.ObjectId }) } Write-Warning $UnresolvedWarning } if ($ClassifiedObjects.Count -ne $UniqueObjects.Count -and $null -ne $WarningMessages) { $WarningMessages.Add([PSCustomObject]@{Type = "Stage-Classification-Parallel"; Message = "Parallel classification returned fewer objects than expected. Expected: $($UniqueObjects.Count), Actual: $($ClassifiedObjects.Count)" }) Write-Warning "Parallel classification returned fewer objects than expected. Expected: $($UniqueObjects.Count), Actual: $($ClassifiedObjects.Count)" } } else { if ($EnableParallelProcessing) { Write-Host "Using sequential classification processing (dataset too small: $($UniqueObjects.Count) objects)" -ForegroundColor Yellow } else { Write-Host "Using sequential classification processing (parallel disabled)..." -ForegroundColor Yellow } $ClassifiedObjects = $UniqueObjects | ForEach-Object { if ($null -ne $_.ObjectId) { $ObjectId = $_.ObjectId if ($VerbosePreference -ne 'SilentlyContinue') { Write-Verbose -Message "Processing classifications for $($ObjectId)..." } # Object types $ObjectDetails = $ObjectDetailsCache[$ObjectId] # Fail-closed: never drop a principal whose object details could not be resolved # (failed/throttled Graph call, $null cache entry). A vanished ControlPlane principal # would otherwise be removed from its protection group by the Administrative Unit / # Conditional Access group reconciliation. Emit a placeholder pinned to ControlPlane # instead, built from the identity fields known from the assignment data. $IsUnresolvedObject = ($null -eq $ObjectDetails) if ($IsUnresolvedObject) { $UnresolvedWarning = "Object $ObjectId could not be resolved - emitted as fail-closed ControlPlane placeholder (TaggedBy=UnresolvedObject)" if ($null -ne $WarningMessages) { $WarningMessages.Add([PSCustomObject]@{ Type = "UnresolvedObject" Message = $UnresolvedWarning Target = $ObjectId }) } Write-Warning $UnresolvedWarning # Shared schema template (see its definition in function scope) - clone and # fill the object-specific fields. $Details = [ordered]@{} foreach ($TemplateKey in $UnresolvedObjectDetailsTemplate.Keys) { $Details[$TemplateKey] = $UnresolvedObjectDetailsTemplate[$TemplateKey] } $Details['ObjectTenantId'] = $_.ObjectTenantId $KnownObjectType = "$($_.ObjectType)" if (-not [string]::IsNullOrWhiteSpace($KnownObjectType)) { $Details['ObjectType'] = $KnownObjectType } $Details['ObjectDisplayName'] = $ObjectId $ObjectDetails = [PSCustomObject]$Details } # RBAC Assignments $RbacClassifiedAssignments = $RbacClassificationsByObject[$ObjectId] # Classification - use hashtable for unique aggregation $UniqueClassificationsHash = @{} foreach ($Assignment in $RbacClassifiedAssignments) { if ($null -ne $Assignment.Classification) { foreach ($ClassItem in $Assignment.Classification) { $key = "$($ClassItem.AdminTierLevel)|$($ClassItem.AdminTierLevelName)|$($ClassItem.Service)" if (-not $UniqueClassificationsHash.ContainsKey($key)) { $UniqueClassificationsHash[$key] = $ClassItem } } } } $Classification = @($UniqueClassificationsHash.Values | Select-Object -Unique -ExcludeProperty TaggedBy, TaggedByObjectIds, TaggedByObjectDisplayNames, TaggedByRoleSystem | Sort-Object AdminTierLevel, AdminTierLevelName, Service) if ($Classification.Count -eq 0) { $Classification = @([PSCustomObject]@{ 'AdminTierLevel' = "Unclassified" 'AdminTierLevelName' = "Unclassified" 'Service' = "Unclassified" }) } if ($IsUnresolvedObject) { # Marker entry flags the fail-closed placeholder in the export. Keep any # classifications derived from the assignments themselves, but drop the generic # "Unclassified" fallback in favor of the explicit marker. $Classification = @($UnresolvedMarkerClassification.PSObject.Copy()) + @($Classification | Where-Object { $_.AdminTierLevelName -ne "Unclassified" }) } New-EntraOpsEAMOutputObject ` -ObjectId $ObjectId ` -ObjectDetails $ObjectDetails ` -Classification $Classification ` -RoleAssignments @($RbacClassifiedAssignments) ` -RoleSystem $RoleSystem } } } return $ClassifiedObjects } # SIG # Begin signature block # MIIniAYJKoZIhvcNAQcCoIIneTCCJ3UCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCB3IACe8UXCLtnA # 2YeVVdgeQme8Z/G6fgl9DGM09POMB6CCIDwwggXJMIIEsaADAgECAhAbtY8lKt8j # 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 # ARUwLwYJKoZIhvcNAQkEMSIEICK9G/0LRMEOal5YsSUXPTwTmOEErkvdvQqX/1sX # zobeMA0GCSqGSIb3DQEBAQUABIIBgJjcrEMIjHz2ll5udwtgqw9FeNIWw3g/SvzH # BPH8PkvNf0SL2cK4YjNKeToGhG3gK+/YoK2hRpfgJ3inC1lDXU8O3jAfabFMKeEh # 99Op0FhyTswIUuoLEsOEx3jrb5CiwGRNCl7Fa53RmbVzjT1q1n6TqwOmMAJ2osJY # FOhGfSjtPSiY2fuVdUAFHdK3rBo4FWcUxgS/wPlDh1poi1BFSr4bzBUJprSXtx3a # 02ZEGBwjDCF4sFW6eCWlLg7SNlzxCIu9JcyPBfKeozn8eg8TBJBzhX06dbN/+U1k # 6nbX5/QK24vzkCne2YZJHw+KiNzlOIii9FGUUHTKu+IQiAW6bJweeoEBsu4IKTw/ # at4GF37Y9+XfrDUR04GA7WyWXE/lKx97mCOSbWiKBvZJw9CkO+IJ/+uPbXc1qPaT # nDBaijw8TGFsdw7g2FV3fQEZXrwo9PCJUts6DZwWVpduTW3hzzEQmZQDD7TRM0z6 # uT8PI6hF5zPts4Px5SdR4LeItl6t26GCBAIwggP+BgkqhkiG9w0BCQYxggPvMIID # 6wIBATBqMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0 # ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQQIQ # KPB3wRw2vf5fdDJHcCcuAzANBglghkgBZQMEAgIFAKCCAVYwGgYJKoZIhvcNAQkD # MQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNjEwMTMwOTQwMDFaMDcG # CyqGSIb3DQEJEAIvMSgwJjAkMCIEIIW+kOEK0kONfMkotq9IsJqyCBd87PiwEmxY # 05EFJcQ8MD8GCSqGSIb3DQEJBDEyBDAPWhBxhcPGXGwqjDYa4cs3FrqVzbKc89qj # aZQUQFQw6ungybqbdHcS8805msKmyv0wgZ8GCyqGSIb3DQEJEAIMMYGPMIGMMIGJ # MIGGBBRXFGhBDKha80JO+RZKUTYQ9NONmDBuMFqkWDBWMQswCQYDVQQGEwJQTDEh # MB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQwIgYDVQQDExtDZXJ0 # dW0gVGltZXN0YW1waW5nIDIwMjEgQ0ECECjwd8EcNr3+X3QyR3AnLgMwDQYJKoZI # hvcNAQEBBQAEggIAFc+CTbZBEeiwNkahUQt37+5t1udoGyk288adPpGd9SFbyb0E # ODpomdgVubd8QtAL/KsOH4B9rGLMcBSD9iv0VfDKbTEPaur3e8DGGgZhho1LJrZp # Y4NGsHDFT/0g+h/mbt3vx2/OPeswExTEgowr20GW4Jaya7O5tvtvbfAUDBjuWH/a # WEnvGneLQtUfTNWeYhL8XWonyNIXBp70hwXETGgaapmgqW7Yz01+lOKtKR6etn3g # 74/VDxG9ryrnxxqNE+d9fvgsTKTIAQINbuPoLZt5ro1e5g22HhShAr0mnKBJp55J # NxA8I2bBRx7gI7ZbmK9fg9cMwNNUZid3DJXiAZKN0W5ga/1nmgvFGh0oY4Srph+w # MX8amRW4qNAlfKsLtoaI1aECdoY7bs+YuWy5Nx/VjJT6QEHk26E/pAon+T5p2395 # HwriEbeBhAzYxlkjBUUrVC2IfkfjPwEiLtw+DxFFUtv4IW3FFHlMA4J1tkWcAXiM # CyDKypJZ6qREustAH1QbAQS7vhjWTyX17C2EUvBgXDZKpJnP1Dl7P3TAqGx2L3sF # GDR4SIGjkrRZ0WAzfcD7EBm0XrZf9QogKgLeE+4I6Qs5/KBl64/RBu1zEeLbWfe0 # /E9dW06UctwUqZMJHUpS484JduhkXk3piy2l6Lf4cdaG1uwxbC1e8FLnWno= # SIG # End signature block |