Private/Save-EntraOpsEAMRbacSystemJson.ps1
|
function Save-EntraOpsEAMRbacSystemJson { <# .SYNOPSIS Saves EAM data for a single RBAC system to JSON files. .DESCRIPTION Shared helper that replaces the ~50-line save block duplicated 5 times in Save-EntraOpsPrivilegedEAMJson. Handles cleanup, directory creation, aggregate JSON export, and parallel per-object JSON writes. .PARAMETER ExportFolder The target export folder for this RBAC system. .PARAMETER RbacSystemName Display name for progress messages (e.g., "EntraID", "Defender"). .PARAMETER EamData The EAM classified objects to save. .PARAMETER AggregateFileName Name of the aggregate JSON file (e.g., "EntraID.json"). .PARAMETER ExportTransitiveByNestingDetails When $true (default), TransitiveByNestingObjectIds and TransitiveByNestingObjectDisplayNames are included in the exported JSON. Set to $false to omit these fields. .PARAMETER ExportTaggedByDetails When $true (default), TaggedByObjectIds, TaggedByObjectDisplayNames, and TaggedByRoleSystem are included in the exported JSON. Set to $false to omit these fields. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string]$ExportFolder, [Parameter(Mandatory = $true)] [string]$RbacSystemName, [Parameter(Mandatory = $true)] [AllowNull()] [object]$EamData, [Parameter(Mandatory = $true)] [string]$AggregateFileName, [Parameter(Mandatory = $false)] [bool]$ExportTransitiveByNestingDetails = $true, [Parameter(Mandatory = $false)] [bool]$ExportTaggedByDetails = $true ) # --- Path safety: ensure ExportFolder is under the expected base directory --- # Resolved against the PowerShell location: GetFullPath would use the process directory $ResolvedExportFolder = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ExportFolder) $ResolvedBaseFolder = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($EntraOpsBaseFolder) # Guard against misconfigured paths that could cause destructive deletion outside the workspace if (-not (Test-EntraOpsPathWithinRoot -Path $ExportFolder -Root $EntraOpsBaseFolder)) { throw "Security check failed: ExportFolder '$ResolvedExportFolder' is not under the expected base directory '$ResolvedBaseFolder'. Aborting to prevent accidental data loss." } # Additional safety: reject obviously dangerous root-level or short paths if ($ResolvedExportFolder.Length -le 4 -or $ResolvedExportFolder -in @('/', '\', 'C:\', "$HOME", "$env:USERPROFILE")) { throw "Security check failed: ExportFolder '$ResolvedExportFolder' resolves to a system-critical path. Aborting." } # Refuse to touch the previous export when the new collection came back empty (error, throttling, # interrupted run): later pipeline stages depend on these files - IdentityGovernance classifies # catalog groups from the PREVIOUS run's Azure/EntraID exports since Azure is collected last - # so deleting a good previous state on an empty result silently degrades their classification. if ($null -eq $EamData -or @($EamData).Count -eq 0) { Write-Warning "Result for $RbacSystemName is empty because of an issue or empty entries in the RBAC system. Keeping previous export in $ExportFolder untouched." return } # Pre-filter (before the previous export is touched, so invalid input can never replace it) $OriginalCount = @($EamData).Count $EamData = @($EamData | Where-Object { $null -ne $_.ObjectType -and $null -ne $_.ObjectId }) $FilteredCount = $EamData.Count if ($FilteredCount -ne $OriginalCount) { Write-Warning "Filtered out $($OriginalCount - $FilteredCount) objects with null ObjectType or ObjectId before saving." } # Validate ObjectType and ObjectId don't contain path traversal characters $PathTraversalPattern = '[/\\:*?"<>|]|\.\.' $SafeData = @($EamData | Where-Object { if ($_.ObjectType -match $PathTraversalPattern) { Write-Warning "Skipping object with unsafe ObjectType: $($_.ObjectType)" return $false } if ($_.ObjectId -match $PathTraversalPattern) { Write-Warning "Skipping object with unsafe ObjectId: $($_.ObjectId)" return $false } return $true }) if ($SafeData.Count -ne $EamData.Count) { Write-Warning "Filtered out $($EamData.Count - $SafeData.Count) objects with path traversal characters in ObjectType or ObjectId." } $EamData = $SafeData if ($EamData.Count -eq 0) { Write-Warning "No valid $RbacSystemName objects remain after validation. Keeping previous export in $ExportFolder untouched." return } $EamData = @($EamData | Sort-Object ObjectDisplayName, ObjectType, ObjectId) # A single-item pipeline result collapses an array into one value, so the JSON shape would depend # on the number of entries and change between runs. Classification collections are always written as arrays. $ArrayShapedEntryProperties = @('MatchedActions', 'ScopedObjects') $NormalizeClassificationShape = { param($Owner) $ClassificationProperty = $Owner.PSObject.Properties['Classification'] if ($null -eq $ClassificationProperty -or $null -eq $ClassificationProperty.Value) { return } # A scalar string is not a collapsed entry but an unsupported shape; it is left for Test-EntraOpsGeneratedArtifacts to reject. if ($ClassificationProperty.Value -is [string]) { return } if ($ClassificationProperty.Value -isnot [array]) { $Owner.Classification = @($ClassificationProperty.Value) } foreach ($Entry in @($Owner.Classification)) { if ($null -eq $Entry) { continue } foreach ($PropertyName in $ArrayShapedEntryProperties) { if ($Entry -is [System.Collections.IDictionary]) { if ($Entry.Contains($PropertyName) -and $null -ne $Entry[$PropertyName] -and $Entry[$PropertyName] -isnot [array]) { $Entry[$PropertyName] = @($Entry[$PropertyName]) } continue } $EntryProperty = $Entry.PSObject.Properties[$PropertyName] if ($null -ne $EntryProperty -and $null -ne $EntryProperty.Value -and $EntryProperty.Value -isnot [array]) { $Entry.$PropertyName = @($EntryProperty.Value) } } } } # Ensure stable RoleAssignments schema so null-only fields are still included in JSON output. foreach ($EamObject in $EamData) { & $NormalizeClassificationShape $EamObject if ($null -eq $EamObject.RoleAssignments) { continue } $SeenRoleAssignments = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) $UniqueRoleAssignments = [System.Collections.Generic.List[object]]::new() foreach ($RoleAssignment in @($EamObject.RoleAssignments)) { if ($null -eq $RoleAssignment) { continue } & $NormalizeClassificationShape $RoleAssignment if (-not $RoleAssignment.PSObject.Properties.Match('RoleAssignmentInstanceId').Count -or [string]::IsNullOrWhiteSpace("$($RoleAssignment.RoleAssignmentInstanceId)")) { $RoleAssignment | Add-Member -NotePropertyName 'RoleAssignmentInstanceId' -NotePropertyValue ( Get-EntraOpsRoleAssignmentInstanceId -RoleSystem $RbacSystemName -RoleAssignment $RoleAssignment ) -Force } if (-not $RoleAssignment.PSObject.Properties.Match('TransitiveByNestingObjectIds').Count) { $RoleAssignment | Add-Member -NotePropertyName 'TransitiveByNestingObjectIds' -NotePropertyValue $null -Force } if (-not $RoleAssignment.PSObject.Properties.Match('TransitiveByNestingObjectDisplayNames').Count) { $RoleAssignment | Add-Member -NotePropertyName 'TransitiveByNestingObjectDisplayNames' -NotePropertyValue $null -Force } # RoleAssignmentInstanceId identifies the normalized source row; the ordered nesting # chain distinguishes separate transitive paths through which that row reaches an object. $NestingObjectIds = @($RoleAssignment.TransitiveByNestingObjectIds | ForEach-Object { if ($null -eq $_) { '' } else { "$($_)".Trim().ToLowerInvariant() } }) $AssignmentKey = @( "$($RoleAssignment.RoleAssignmentInstanceId)".Trim().ToLowerInvariant(), ($NestingObjectIds -join [char]0x1E) ) -join [char]0x1F if ($SeenRoleAssignments.Add($AssignmentKey)) { $UniqueRoleAssignments.Add($RoleAssignment) } } if ($UniqueRoleAssignments.Count -ne @($EamObject.RoleAssignments).Count) { Write-Verbose "Removed $(@($EamObject.RoleAssignments).Count - $UniqueRoleAssignments.Count) exact duplicate role assignment row(s) for object '$($EamObject.ObjectId)' in $RbacSystemName." } $EamObject.RoleAssignments = $UniqueRoleAssignments.ToArray() } # Strip optional detail properties based on export flags $ExcludeProperties = @() if (-not $ExportTransitiveByNestingDetails) { $ExcludeProperties += 'TransitiveByNestingObjectIds', 'TransitiveByNestingObjectDisplayNames' } if (-not $ExportTaggedByDetails) { $ExcludeProperties += 'TaggedByObjectIds', 'TaggedByObjectDisplayNames', 'TaggedByRoleSystem' } if ($ExcludeProperties.Count -gt 0) { $EamData = @($EamData | Select-Object -ExcludeProperty $ExcludeProperties) } # Write to a staging folder and replace the previous export only after every file was written $ExportParentFolder = Split-Path -Path $ResolvedExportFolder -Parent $ExportFolderName = Split-Path -Path $ResolvedExportFolder -Leaf $OperationId = [guid]::NewGuid().ToString('N') $StagingFolder = Join-Path $ExportParentFolder ".$ExportFolderName.staging-$OperationId" $BackupFolder = Join-Path $ExportParentFolder ".$ExportFolderName.backup-$OperationId" try { New-Item -Path $StagingFolder -ItemType Directory -Force | Out-Null # Save aggregate JSON; -InputObject keeps a single object as a one-element array ConvertTo-Json -InputObject $EamData -Depth 10 | Out-File -Path (Join-Path $StagingFolder $AggregateFileName) -Force -ErrorAction Stop # Create subdirectories per object type $EamData | Group-Object ObjectType | ForEach-Object { New-Item -ItemType Directory -Force -Path (Join-Path $StagingFolder $_.Name) | Out-Null } # Parallel per-object JSON writes. The runspaces have no access to module-private functions, # so the containment prefix is precomputed here instead of calling Test-EntraOpsPathWithinRoot. $StagingFolderPrefix = [System.IO.Path]::TrimEndingDirectorySeparator($StagingFolder) + [System.IO.Path]::DirectorySeparatorChar $Results = $EamData | ForEach-Object -Parallel { $Obj = $_ $Path = "$using:StagingFolder/$($Obj.ObjectType)/$($Obj.ObjectId).json" # Final resolved-path check to guard against path traversal $ResolvedPath = [System.IO.Path]::GetFullPath($Path) if (-not $ResolvedPath.StartsWith($using:StagingFolderPrefix, [System.StringComparison]::Ordinal)) { Write-Warning "Path traversal detected for ObjectId '$($Obj.ObjectId)', skipping." return $false } try { $Obj | ConvertTo-Json -Depth 10 | Out-File -Path $Path -Force -ErrorAction Stop $true } catch { Write-Warning "Failed to save file for $($Obj.ObjectId): $_" $false } } -ThrottleLimit 50 $SuccessCount = @($Results | Where-Object { $_ -eq $true }).Count if ($SuccessCount -ne $EamData.Count) { throw "Parallel file write had failures. Expected: $($EamData.Count), Success: $SuccessCount. Keeping previous export in $ExportFolder untouched." } if (Test-Path -LiteralPath $ResolvedExportFolder) { Write-Host "Replacing previous export in $ExportFolder..." -ForegroundColor Gray Move-Item -LiteralPath $ResolvedExportFolder -Destination $BackupFolder -ErrorAction Stop } try { Move-Item -LiteralPath $StagingFolder -Destination $ResolvedExportFolder -ErrorAction Stop } catch { if (Test-Path -LiteralPath $BackupFolder) { Move-Item -LiteralPath $BackupFolder -Destination $ResolvedExportFolder -ErrorAction SilentlyContinue } throw "Failed to replace the export in $ExportFolder. The previous export was restored. Error: $($_.Exception.Message)" } } finally { if (Test-Path -LiteralPath $StagingFolder) { Remove-Item -LiteralPath $StagingFolder -Recurse -Force -ErrorAction SilentlyContinue } # The backup is the only copy of the previous export until an export folder is in place again. if (Test-Path -LiteralPath $BackupFolder) { if (Test-Path -LiteralPath $ResolvedExportFolder) { Remove-Item -LiteralPath $BackupFolder -Recurse -Force -ErrorAction SilentlyContinue } else { Write-Warning "The previous export of $RbacSystemName could not be restored and was kept in $BackupFolder." } } } Write-Host "Saved $($EamData.Count) $RbacSystemName objects to $ExportFolder" -ForegroundColor Green } # SIG # Begin signature block # MIIniAYJKoZIhvcNAQcCoIIneTCCJ3UCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAtwV48tZIyhuZF # lYDmRBaP4UEGVvex3CR4MjrVGGpYf6CCIDwwggXJMIIEsaADAgECAhAbtY8lKt8j # 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 # ARUwLwYJKoZIhvcNAQkEMSIEIKEIpC8K/YXD0Qf1wr8EnIxzstFyZzpRED2FeTSW # s0oWMA0GCSqGSIb3DQEBAQUABIIBgFe0krf7XR/xrtN/hgzMyRS5nsOZsXJjH8i3 # orAj/XUWzIwMMPxzn0JhlCohoAbLbPfs3dfyCvOxNn1sjK5rm0wdNqui0R6c1S66 # iBUj5Uc1uy4pUmXOkSEEBLwU6N6TtgNhjZM+uufnF+JXaWEDhO44P2AsYFbMBvHV # tmz/SIcWyhYTiTqWJBuaOyU/F4+p+Z5Dcgnx3+SVszsiEZe0yrSc74FUKO6jXcV3 # xh99gCR0j4FSA5CRmbwT2kTVpvcJZewYr/hI44XW7cAMvnw27YAmKe5S3zXdBcps # e0YHrT5uTkV0UAfb5vH3DWrD//PvKF/1WcvwmIr8UNTQ7WEhNXbW+xic4msS17DA # gAuLQ6/bFpHX48GBjO8t6BoPCxbZ2JDp0r/784mSIsAFFoITh81d1ffo+dDytG19 # C4E/9W4HpnUTfcI43DSLzSFUgRyfuijZ87ID6tzueBj77aaQ2cdUDMCUmy31gi6F # NAIMmisz9fV1tlNncq5rCcTCuBCbcqGCBAIwggP+BgkqhkiG9w0BCQYxggPvMIID # 6wIBATBqMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0 # ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQQIQ # KPB3wRw2vf5fdDJHcCcuAzANBglghkgBZQMEAgIFAKCCAVYwGgYJKoZIhvcNAQkD # MQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNjEwMjIxMTM4MjFaMDcG # CyqGSIb3DQEJEAIvMSgwJjAkMCIEIIW+kOEK0kONfMkotq9IsJqyCBd87PiwEmxY # 05EFJcQ8MD8GCSqGSIb3DQEJBDEyBDBOa3IFbcngsNtP2S6i1aSOo8t0CqqmJjDr # frFWIMxOdW/XtifS0A7uXPfeiEfuvOwwgZ8GCyqGSIb3DQEJEAIMMYGPMIGMMIGJ # MIGGBBRXFGhBDKha80JO+RZKUTYQ9NONmDBuMFqkWDBWMQswCQYDVQQGEwJQTDEh # MB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQwIgYDVQQDExtDZXJ0 # dW0gVGltZXN0YW1waW5nIDIwMjEgQ0ECECjwd8EcNr3+X3QyR3AnLgMwDQYJKoZI # hvcNAQEBBQAEggIANarg0+gphAeW9wHMeONw2kN0YwjVXa7AB3Bu9by1IgH8MYeS # Axzj9g10SkpUfvn8TFOQolrXlu6Hfdh4WPCMCLeu8eZSvv/+887vz+wDwAMCRbgx # aBQurUCf2BkayQDvQ3r/XzoHtkc52hyoUXT+Ky0v0pQp878SLXozPEAxAXIhceQC # pAiB2QqXKapcvn6OpZQKiS7VeJP6Si3OAygg1m6l4nd6Ye9+PzzkRQCPf7v5LLEG # svbmnBDuwVUTNfWaoMvYQ4z9uhKSRyHhT+sGse+K3XlqC8nOqFMHvudCR3dubUaU # 81yi+lldc4RfZV9CqQnRXuNs5JR6VS1ga+0X1vmUTvppvnq94MAI3bsC7PS23n2M # 30IG+RejqMV3GNGCzzYTXDy3DUTgbezCpTNoT+50uPf83ciJCTMpr5c1KKePcman # bBRHjwRXE9gHe0CnPz5G+bugE3K00tnmwdbjzK05EB5yTMZaZWr77Yjtz8GHimHn # plIOoqY+ATP2ta8MbB9E/TvcY4w5SPTTSHpnvmA4cNBWMgQGj0Ah7/X607cC3igw # hFEjS96Eo3QZtuqXVKvg1dRPutL/i9i/cSMd4CtOpM12GVGIDOrhkqyPejjqVVCV # x/oBb4tGMeo52dHSxHhknaC0dEZy5/l2EYqtxUxg2OXISbmC9rb9hD0u5gY= # SIG # End signature block |