Public/Validation/Test-EntraOpsGeneratedArtifacts.ps1
|
function Test-EntraOpsGeneratedArtifacts { [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [string]$PrivilegedEamPath = './PrivilegedEAM', [Parameter(Mandatory = $false)] [switch]$FailOnContradictoryTierPair, [Parameter(Mandatory = $false)] [string]$TenantGovernancePath, [Parameter(Mandatory = $false)] [switch]$FailOnPrivilegedAssignmentWithoutClassification ) $CanonicalTierLevelByName = @{ ControlPlane = '0' ManagementPlane = '1' WorkloadPlane = '1' UserAccess = '2' Unclassified = 'Unclassified' } $Violations = [System.Collections.Generic.List[string]]::new() $TierPairWarnings = [System.Collections.Generic.List[string]]::new() $DataQualityWarnings = [System.Collections.Generic.List[string]]::new() $ObjectCount = 0 $AssignmentCount = 0 $TenantGovernanceResourceCount = 0 if ([string]::IsNullOrWhiteSpace($PrivilegedEamPath) -and [string]::IsNullOrWhiteSpace($TenantGovernancePath)) { throw 'Nothing to validate: pass -PrivilegedEamPath and/or -TenantGovernancePath.' } $ResolvedPrivilegedEamPath = if ([string]::IsNullOrWhiteSpace($PrivilegedEamPath)) { $null } else { $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($PrivilegedEamPath) } if ($ResolvedPrivilegedEamPath) { if (-not (Test-Path -LiteralPath $ResolvedPrivilegedEamPath -PathType Container)) { throw "Privileged EAM path '$ResolvedPrivilegedEamPath' does not exist." } $AggregateFiles = @( Get-ChildItem -LiteralPath $ResolvedPrivilegedEamPath -Directory | ForEach-Object { $AggregatePath = Join-Path $_.FullName "$($_.Name).json" if (Test-Path -LiteralPath $AggregatePath -PathType Leaf) { Get-Item -LiteralPath $AggregatePath } } ) if ($AggregateFiles.Count -eq 0) { throw "No RBAC-system aggregate JSON files were found under '$ResolvedPrivilegedEamPath'." } foreach ($AggregateFile in $AggregateFiles) { $SeenObjectIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) try { $EamObjects = @([System.IO.File]::ReadAllText($AggregateFile.FullName) | ConvertFrom-Json -Depth 100 -ErrorAction Stop) } catch { $Violations.Add("$($AggregateFile.FullName): invalid JSON - $($_.Exception.Message)") continue } foreach ($EamObject in $EamObjects) { $ObjectCount++ $ObjectId = "$($EamObject.ObjectId)".Trim() if ([string]::IsNullOrWhiteSpace($ObjectId)) { $Violations.Add("$($AggregateFile.Name): an object is missing ObjectId.") $ObjectId = '<missing>' } elseif (-not $SeenObjectIds.Add($ObjectId)) { $Violations.Add("$($AggregateFile.Name): duplicate object '$ObjectId' appears in the RBAC-system aggregate.") } $TierName = "$($EamObject.ObjectAdminTierLevelName)" $TierLevel = "$($EamObject.ObjectAdminTierLevel)" if (-not $CanonicalTierLevelByName.ContainsKey($TierName) -or $TierLevel -ne $CanonicalTierLevelByName[$TierName]) { # Most commonly caused by two independently-tagged Custom Security Attribute fields # drifting apart on the source object (see EntraOpsConfig.json CustomSecurityAttributes # section and docs/core.html "Classify by Custom Security Attributes" for the paired # *AdminTierLevelAttribute / *AdminTierLevelNameAttribute field names to check and fix # at the source in Microsoft Entra) - or, if enabled, an AlternateObjectTierLevelAttributes # filter expression. This is tenant data, not a code defect: fix the tagging, then re-run. $Message = "$($AggregateFile.Name): object '$ObjectId' has contradictory tier pair '$TierLevel/$TierName' - check this object's Custom Security Attribute (or AlternateObjectTierLevelAttributes filter) values; see docs/core.html 'Classify by Custom Security Attributes'." if ($FailOnContradictoryTierPair) { $Violations.Add($Message) } else { $TierPairWarnings.Add($Message) } } $SeenAssignmentPaths = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) foreach ($RoleAssignment in @($EamObject.RoleAssignments)) { if ($null -eq $RoleAssignment) { continue } $AssignmentCount++ $InstanceId = "$($RoleAssignment.RoleAssignmentInstanceId)".Trim().ToLowerInvariant() if ([string]::IsNullOrWhiteSpace($InstanceId)) { $Violations.Add("$($AggregateFile.Name): object '$ObjectId' has a role assignment without RoleAssignmentInstanceId.") continue } $Classification = $RoleAssignment.Classification $ClassificationIsEmpty = $null -eq $Classification -or ($Classification -is [array] -and $Classification.Count -eq 0) -or ($Classification -is [System.Collections.IDictionary] -and $Classification.Count -eq 0) -or ($Classification -is [pscustomobject] -and @($Classification.PSObject.Properties).Count -eq 0) $ClassificationHasSupportedShape = $null -eq $Classification -or $Classification -is [array] -or $Classification -is [System.Collections.IDictionary] -or $Classification -is [pscustomobject] if (-not $ClassificationHasSupportedShape) { $Violations.Add("$($AggregateFile.Name): assignment '$InstanceId' has unsupported Classification shape '$($Classification.GetType().FullName)'.") } if ($RoleAssignment.RoleIsPrivileged -eq $true -and $ClassificationIsEmpty) { $Message = "$($AggregateFile.Name): privileged assignment '$InstanceId' ('$($RoleAssignment.RoleDefinitionName)') for object '$ObjectId' has no classification result." if ($FailOnPrivilegedAssignmentWithoutClassification) { $Violations.Add($Message) } else { $DataQualityWarnings.Add($Message) } } $NestingObjectIds = @($RoleAssignment.TransitiveByNestingObjectIds | ForEach-Object { if ($null -eq $_) { '' } else { "$($_)".Trim().ToLowerInvariant() } }) $AssignmentPathKey = @($InstanceId, ($NestingObjectIds -join [char]0x1E)) -join [char]0x1F if (-not $SeenAssignmentPaths.Add($AssignmentPathKey)) { $Violations.Add("$($AggregateFile.Name): object '$ObjectId' contains duplicate assignment path '$InstanceId'.") } } } } } if (-not [string]::IsNullOrWhiteSpace($TenantGovernancePath)) { $ResolvedTenantGovernancePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($TenantGovernancePath) if (-not (Test-Path -LiteralPath $ResolvedTenantGovernancePath -PathType Container)) { throw "Tenant Governance path '$ResolvedTenantGovernancePath' does not exist." } $ManifestPath = Join-Path $ResolvedTenantGovernancePath '.SnapshotManifest.json' if (-not (Test-Path -LiteralPath $ManifestPath -PathType Leaf)) { $Violations.Add("Tenant Governance snapshot manifest is missing: $ManifestPath") } else { try { $SnapshotManifest = Get-Content -LiteralPath $ManifestPath -Raw | ConvertFrom-Json -Depth 100 -ErrorAction Stop } catch { $Violations.Add("$ManifestPath`: invalid JSON - $($_.Exception.Message)") } } $IdentityFiles = @{} $ActualCounts = @{} foreach ($ResourceFile in @(Get-ChildItem -LiteralPath $ResolvedTenantGovernancePath -Filter '*.json' -File -Recurse | Where-Object { -not $_.Name.StartsWith('.') -and $_.FullName -notmatch '[\\/]\.(?:staging|backup)-' })) { # -AsHashtable accepts valid JSON objects that contain an empty property # name. Microsoft Graph emits those in some Tenant Governance payloads. try { $Resource = [System.IO.File]::ReadAllText($ResourceFile.FullName) | ConvertFrom-Json -AsHashtable -Depth 100 -ErrorAction Stop } catch { $Violations.Add("$($ResourceFile.FullName): invalid JSON - $($_.Exception.Message)"); continue } $TenantGovernanceResourceCount++ $ResourceType = "$($Resource.resourceType)".Trim().ToLowerInvariant() $RelativePath = [System.IO.Path]::GetRelativePath($ResolvedTenantGovernancePath, $ResourceFile.FullName) -replace '\\', '/' $FolderResourceType = ($RelativePath -split '/')[0].ToLowerInvariant() if ([string]::IsNullOrWhiteSpace($ResourceType)) { $ResourceType = $FolderResourceType } elseif ($ResourceType -ne $FolderResourceType) { $Violations.Add("$($ResourceFile.FullName): resourceType '$ResourceType' does not match its top-level snapshot folder '$FolderResourceType'.") } if (-not $ActualCounts.ContainsKey($ResourceType)) { $ActualCounts[$ResourceType] = 0 } $ActualCounts[$ResourceType]++ $ResourceId = "$($Resource.properties.Id)".Trim().ToLowerInvariant() if ([string]::IsNullOrWhiteSpace($ResourceId)) { continue } $Identity = "$ResourceType|$ResourceId" if (-not $IdentityFiles.ContainsKey($Identity)) { $IdentityFiles[$Identity] = [System.Collections.Generic.List[string]]::new() } $IdentityFiles[$Identity].Add($ResourceFile.FullName) } foreach ($Identity in $IdentityFiles.Keys) { if ($IdentityFiles[$Identity].Count -gt 1) { $Violations.Add("Tenant Governance identity '$Identity' is stored in $($IdentityFiles[$Identity].Count) files: $($IdentityFiles[$Identity] -join ', ')") } } if ($SnapshotManifest) { $ManifestCountProperties = @($SnapshotManifest.PublishedResourceTypeCounts.PSObject.Properties) $ManifestCountResourceTypes = @($ManifestCountProperties | ForEach-Object { $_.Name.ToLowerInvariant() }) foreach ($CountProperty in $ManifestCountProperties) { $ResourceType = $CountProperty.Name.ToLowerInvariant() $ActualCount = if ($ActualCounts.ContainsKey($ResourceType)) { $ActualCounts[$ResourceType] } else { 0 } if ([int]$CountProperty.Value -ne $ActualCount) { $Violations.Add("Tenant Governance manifest count for '$ResourceType' is $($CountProperty.Value), but $ActualCount JSON file(s) exist.") } } # Folders of resource types that are no longer configured are intentionally preserved by # Save-EntraOpsTenantGovernanceSnapshotJson and reported as NotInConfig by the snapshot # report. They are not part of the manifest counts, so they are reported here without # failing the run; their files were still checked for JSON validity and duplicate identities. $ManifestResourceFileCount = 0 foreach ($ResourceType in @($ActualCounts.Keys | Sort-Object)) { if ($ManifestCountResourceTypes -contains $ResourceType) { $ManifestResourceFileCount += $ActualCounts[$ResourceType] } else { $DataQualityWarnings.Add("Tenant Governance resource type '$ResourceType' has $($ActualCounts[$ResourceType]) JSON file(s) on disk but is not part of the current snapshot manifest (removed from the configured resource types). Delete the folder to stop tracking it.") } } if ($SnapshotManifest.IsComplete -eq $true -and [int]$SnapshotManifest.CapturedResourceCount -ne $ManifestResourceFileCount) { $Violations.Add("Complete Tenant Governance manifest captured count is $($SnapshotManifest.CapturedResourceCount), but $ManifestResourceFileCount resource file(s) exist for the manifest's resource types.") } } } if ($Violations.Count -gt 0) { throw "Generated artifact integrity validation failed with $($Violations.Count) violation(s):`n$($Violations -join "`n")" } foreach ($Warning in $TierPairWarnings) { Write-Warning $Warning } foreach ($Warning in $DataQualityWarnings) { Write-Warning $Warning } $SummaryParts = @() if ($ResolvedPrivilegedEamPath) { $SummaryParts += "$ObjectCount object(s) and $AssignmentCount role assignment(s) across $($AggregateFiles.Count) RBAC system(s)" } if (-not [string]::IsNullOrWhiteSpace($TenantGovernancePath)) { $SummaryParts += "$TenantGovernanceResourceCount Tenant Governance resource(s)" } Write-Output "Validated $($SummaryParts -join ' and ')." } # SIG # Begin signature block # MIIniAYJKoZIhvcNAQcCoIIneTCCJ3UCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBL/G58hyQiWsHu # x18/UNO/XbKTc4cSz9M/iJyXXMCYVKCCIDwwggXJMIIEsaADAgECAhAbtY8lKt8j # 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 # ARUwLwYJKoZIhvcNAQkEMSIEIM7o/tksjlAA+RD6OSaj2wzah4nHlH7LiKtq6dZn # 7qR4MA0GCSqGSIb3DQEBAQUABIIBgF8xczAagmmfV7b574VCeTDLXcp+Tbft33+B # d43VQH54uveeG+yzww1Eh6RBQqyWoKS3aCnIwSxWH5AYIfmQKgaUDijcG6hjgmlh # JxDh+wjWlPOXtkoMktsTShUWDTCjRgT5/GDDRU30koImTkw+3TuqiUL8E4A6o6bm # c/Wv+KuMpJUeM+vebydzSOJeFUSwqM93+380KmH0vuRXuJDFOWFgmR8fChROP/r1 # f/QMN9+aqiUxk7YEnnQKBa5EUQtA95AFCkPHd+IcezB+6p0BgWJJcWqgo8aiwyIq # u3DP1v0leqJTypbU7Y/+IW4XtngWHCMyAlT5sVp1KTh6k8rLB6bvfF0ZUnK6LLDa # 5bBVL1j8m0tNMa0lxNV0nT5QJyHjjmhlwMXcw4wn7jjhD4U40QTO8Kl4gyKxrPCK # Z1Lc+LGwYvnhgIdyh4FlSlWGOzF5Dtb6ym8uekurIxh6YixV1AIk3IxZKOkbADaK # 1WZBadjx/sTRPNXFKgWhqg38aFjvlaGCBAIwggP+BgkqhkiG9w0BCQYxggPvMIID # 6wIBATBqMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0 # ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQQIQ # KPB3wRw2vf5fdDJHcCcuAzANBglghkgBZQMEAgIFAKCCAVYwGgYJKoZIhvcNAQkD # MQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNjEwMTMwOTQyMTVaMDcG # CyqGSIb3DQEJEAIvMSgwJjAkMCIEIIW+kOEK0kONfMkotq9IsJqyCBd87PiwEmxY # 05EFJcQ8MD8GCSqGSIb3DQEJBDEyBDBdCD0ZonlsF/zU4KEDcEPR+11jRjd8itTn # 3AkehEpyFyyjEp8+Gxrk7k+4BHiE1pkwgZ8GCyqGSIb3DQEJEAIMMYGPMIGMMIGJ # MIGGBBRXFGhBDKha80JO+RZKUTYQ9NONmDBuMFqkWDBWMQswCQYDVQQGEwJQTDEh # MB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQwIgYDVQQDExtDZXJ0 # dW0gVGltZXN0YW1waW5nIDIwMjEgQ0ECECjwd8EcNr3+X3QyR3AnLgMwDQYJKoZI # hvcNAQEBBQAEggIAs9Ds0IDzTQsFp6ZxZXO71b9N1uCNGjOOzgQ2+BAr1iKE4DZL # jVC470LJAIRgEums9rQN8ACdV8IELWN6IEeoQSXzSvi+0NbdKkOYztX+KOSGqxYo # Bin+iscCZISLv6oj8AOCk0WzNkSyu1kWQ4NCBnAR+pjP7Q2dLgVxZeZK7CXXnXFC # SMrXuBxdJMMwWxq9z7jugjjLYhcUCi3ZgxKAu3rP/n429Mxpa3KpHQjW/2JtTXgp # z7XN8xTO/Xg8eHqCi8y4OHqVz5k76H0cprd7zdCn3YTKoHJwWzOBeVyrFI2LUbmG # EzXOEHfydTwyU57z4+JC3Su68/qXckR/XUnBRNpcwem0/u8/33krmXz7gk0JAtKi # 1veQohJTab+snv1/UuVwAJHNI12cJAKo1DCb7+pS9J2JT6MKPArUV5S+r8RhOgxK # fOmclnaoVAZ0x78E6+Fe71F3zDhFm99xneT+noXA+Y/QMNv2QkjuoVuKxuFxC2E7 # tvRzWLUyFVgjvCYnrXhcEPx3WU/oacMVx3Owx8a2WsEnM55Zht2ICuPeNoj12snq # DGBdEAmPJPpuVc38dBp89WPuAlf/7p6sxqzmOuCgDLMwmuXtjbnzoa0Y9Yib3MG3 # BsYdzdaVpuIEvqOB8kM8w7AvP/G6/q7UIfGqTWwEQaY+jfhLRGOH+G4URxg= # SIG # End signature block |