Public/Reportings/New-EntraOpsTierBreachAnalyzerData.ps1

<#
.SYNOPSIS
    Generate (refresh) the embedded dataset for the EntraOps Tier Breach Analyzer static web app.

.DESCRIPTION
    Transforms EntraOps Privileged EAM data into the Tier Breach Analyzer dataset.

    Generates data/tier-breach-data.js exposing `window.ENTRAOPS_TB_DATA` so the
    static web app works both when served over HTTP (for example Azure Static
    Web Apps) and when opened directly from the file system (no fetch / CORS
    required).

    Flow modeled (left -> right):
        Object Tier -> Object -> Role -> Service -> Service Tier

    Tiering rules:
      * Object designated tier comes from `ObjectAdminTierLevel`.
      * If an object has no classification (empty / "Unclassified"), it is
        treated as Tier 2 (User Access) which is the lowest / least privileged
        tier.
      * A "tier breach" is a path where the object's tier is LESS privileged
        (higher tier number) than the service it can reach through a role.
        The headline case is a Tier 1 / Tier 2 object reaching a Tier 0 service.

    Source data is the Privileged EAM export written by
    Save-EntraOpsPrivilegedEAMJson: PrivilegedEAM/<RbacSystem>/<RbacSystem>.json
    (only JSON files directly in the RBAC system folder are read, not the
    per-object subfolders user/, group/, serviceprincipal/, ...).

    This function requires the module to be imported from a repository checkout that contains the
    Reports/TierBreachAnalyzer folder.

.PARAMETER RepoRoot
    Path to the EntraOps repository root that contains the PrivilegedEAM export folder.
    Defaults to the repository this module lives in.

.PARAMETER ImportPath
    Folder with the Privileged EAM export. Defaults to <RepoRoot>/PrivilegedEAM.

.PARAMETER AppRoot
    Path to the TierBreachAnalyzer app folder (where the generated content is written).
    Defaults to Reports/TierBreachAnalyzer in the EntraOps repository.

.PARAMETER OutFile
    Output file. Defaults to <AppRoot>/data/tier-breach-data.js.

.PARAMETER PassThru
    Emit the generated payload object to the pipeline.

.EXAMPLE
    New-EntraOpsTierBreachAnalyzerData

    Regenerates the Tier Breach Analyzer dataset from the PrivilegedEAM folder of this repository.

.EXAMPLE
    New-EntraOpsTierBreachAnalyzerData -ImportPath "C:\Exports\PrivilegedEAM" -Verbose -WhatIf

    Shows what would be generated from an explicit Privileged EAM export without changing any files.
#>


function New-EntraOpsTierBreachAnalyzerData {

    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $false)]
        [System.String]$RepoRoot,

        [Parameter(Mandatory = $false)]
        [System.String]$ImportPath,

        [Parameter(Mandatory = $false)]
        [System.String]$AppRoot,

        [Parameter(Mandatory = $false)]
        [System.String]$OutFile,

        [Parameter(Mandatory = $false)]
        [switch]$PassThru
    )

    # Resolve the app/repository location relative to the module location:
    # <repo>/EntraOps/Public/<subfolder> -> <repo>/Reports/TierBreachAnalyzer
    # Prefer the module's own ModuleBase (always populated for exported module
    # functions) over $PSScriptRoot, which can be empty depending on how the
    # function was invoked/loaded.
    $ModuleRoot = $MyInvocation.MyCommand.Module.ModuleBase
    if ([string]::IsNullOrWhiteSpace($ModuleRoot) -and -not [string]::IsNullOrWhiteSpace($PSScriptRoot)) {
        $ModuleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
    }
    if ([string]::IsNullOrWhiteSpace($ModuleRoot)) {
        throw "Unable to resolve the EntraOps module location. Import the module with 'Import-Module <path-to-EntraOps> -Force' and try again."
    }
    $RepositoryRoot = Split-Path -Parent $ModuleRoot

    if ([string]::IsNullOrWhiteSpace($RepoRoot)) { $RepoRoot = $RepositoryRoot }
    if ([string]::IsNullOrWhiteSpace($AppRoot)) { $AppRoot = Join-Path $RepositoryRoot 'Reports/TierBreachAnalyzer' }
    if (-not (Test-Path -LiteralPath $AppRoot -PathType Container)) {
        throw "Tier Breach Analyzer app folder not found: $AppRoot. Import the EntraOps module from a repository checkout that contains Reports/TierBreachAnalyzer."
    }
    if ([string]::IsNullOrWhiteSpace($ImportPath)) { $ImportPath = Join-Path $RepoRoot 'PrivilegedEAM' }
    if ([string]::IsNullOrWhiteSpace($OutFile)) { $OutFile = Join-Path $AppRoot 'data/tier-breach-data.js' }

    if (-not (Test-Path -LiteralPath $ImportPath)) {
        throw "Privileged EAM import path not found: $ImportPath. Run Save-EntraOpsPrivilegedEAMJson first or pass -ImportPath."
    }

    Write-Verbose "Reading Privileged EAM JSON from $ImportPath."
    Write-Verbose "Writing Tier Breach Analyzer data to $OutFile."

    # Tier numeric value -> label. Lower number = more privileged.
    $TierLabels = [ordered]@{
        '0' = 'Tier 0 - Control Plane'
        '1' = 'Tier 1 - Management Plane'
        '2' = 'Tier 2 - User Access'
    }

    function Get-PropValue {
        # Safe property access for objects from ConvertFrom-Json whose schema can
        # vary between RBAC systems (also strict-mode proof).
        param($Object, [string] $Name)
        $p = $Object.PSObject.Properties[$Name]
        if ($null -ne $p) { return $p.Value }
        return $null
    }

    function ConvertTo-TierNumber {
        <#
            Normalize a raw tier value to a numeric tier (0/1/2).
            Empty string, $null and "Unclassified" map to the default (Tier 2 -
            lowest privilege), matching the handling of unclassified objects.
        #>

        param(
            $Value,
            [int] $Default = 2
        )
        if ($null -eq $Value) { return $Default }
        $s = "$Value".Trim()
        if ($s -eq '' -or $s -eq 'Unclassified') { return $Default }
        $n = 0
        if (-not [int]::TryParse($s, [ref] $n)) { return $Default }
        if ($n -in 0, 1, 2) { return $n }
        return $Default
    }

    # Match PrivilegedEAM/<System>/<System>.json only (one level deep), not the
    # per-object subfolders (user/, group/, serviceprincipal/ ...).
    $files = Get-ChildItem -Path $ImportPath -Directory |
    ForEach-Object { Get-ChildItem -Path $_.FullName -Filter '*.json' -File } |
    Sort-Object FullName

    if (-not $files) {
        Write-Warning "No Privileged EAM JSON files found under $ImportPath (expected <RbacSystem>/<RbacSystem>.json). Generating an empty dataset."
    } else {
        Write-Verbose "Found $($files.Count) Privileged EAM JSON file(s) to process."
    }

    $objects = [System.Collections.Generic.List[object]]::new()
    $paths = [System.Collections.Generic.List[object]]::new()
    # Maps assignment scope IDs to reusable scope-reasoning entries.
    $scopeReasoningByScope = [ordered]@{}
    $scopeReasoningDetails = Import-EntraOpsAzureScopeReasoning -RepoRoot $RepoRoot

    Write-Verbose "Loaded $($scopeReasoningDetails.Count) Azure scope-reasoning detail record(s)."

    $fileNumber = 0
    foreach ($file in @($files)) {
        $fileNumber++
        $roleSystem = Split-Path -Leaf (Split-Path -Parent $file.FullName)
        Write-Verbose "[$fileNumber/$($files.Count)] Reading $roleSystem data from $($file.FullName)."

        try {
            $data = Get-Content -LiteralPath $file.FullName -Raw -Encoding UTF8 |
            ConvertFrom-Json
        } catch {
            # Stop generation when an enumerated export cannot be read or parsed.
            throw "Failed to read or parse Privileged EAM export file '$($file.FullName)' (RBAC system '$roleSystem'): $($_.Exception.Message)"
        }

        $sourceObjectCount = @($data).Count
        $processedObjectCount = 0
        $pathsBeforeFile = $paths.Count
        foreach ($o in @($data)) {
            $processedObjectCount++
            if ($processedObjectCount % 250 -eq 0) {
                Write-Verbose "[$fileNumber/$($files.Count)] Processed $processedObjectCount of $sourceObjectCount $roleSystem source object(s)."
            }

            $objType = Get-PropValue $o 'ObjectType'
            # Only users and service principals.
            if ($objType -notin 'user', 'serviceprincipal') { continue }

            $objectId = Get-PropValue $o 'ObjectId'
            $display = Get-PropValue $o 'ObjectDisplayName'
            if (-not $display) { $display = $objectId }
            $objTier = ConvertTo-TierNumber -Value (Get-PropValue $o 'ObjectAdminTierLevel')

            $objects.Add([ordered]@{
                    id     = $objectId
                    name   = $display
                    type   = $objType
                    system = $roleSystem
                    tier   = $objTier
                })

            foreach ($ra in @(Get-PropValue $o 'RoleAssignments')) {
                if ($null -eq $ra) { continue }
                $roleName = Get-PropValue $ra 'RoleDefinitionName'
                if (-not $roleName) { $roleName = '(unnamed role)' }
                $isPriv = [bool] (Get-PropValue $ra 'RoleIsPrivileged')
                $classifications = @(Get-PropValue $ra 'Classification')
                if ($classifications.Count -eq 0) { continue }

                # Assignment-level context (explains which assignment causes the breach).
                $scopeId = Get-PropValue $ra 'RoleAssignmentScopeId'
                $scopeName = Get-PropValue $ra 'RoleAssignmentScopeName'
                if (-not $scopeName) { $scopeName = $scopeId }
                $assignType = "$(Get-PropValue $ra 'RoleAssignmentType')"
                $assignSub = "$(Get-PropValue $ra 'RoleAssignmentSubType')"
                $pimManaged = [bool] (Get-PropValue $ra 'PIMManagedRole')
                $pimType = "$(Get-PropValue $ra 'PIMAssignmentType')"
                $roleType = "$(Get-PropValue $ra 'RoleType')"
                $assignId = Get-PropValue $ra 'RoleAssignmentId'
                $assignInstanceId = if (Get-PropValue $ra 'RoleAssignmentInstanceId') { Get-PropValue $ra 'RoleAssignmentInstanceId' } else { Get-EntraOpsRoleAssignmentInstanceId -RoleSystem $roleSystem -RoleAssignment $ra }
                $transitiveBy = "$(Get-PropValue $ra 'TransitiveByObjectDisplayName')"

                # De-duplicate services within a single assignment.
                $seen = [System.Collections.Generic.HashSet[string]]::new()
                foreach ($c in $classifications) {
                    if ($null -eq $c) { continue }
                    $service = Get-PropValue $c 'Service'
                    if (-not $service) { $service = '(unclassified)' }
                    $svcTier = ConvertTo-TierNumber -Value (Get-PropValue $c 'AdminTierLevel')
                    $key = "$service|$svcTier"
                    if (-not $seen.Add($key)) { continue }

                    $taggedBy = "$(Get-PropValue $c 'TaggedBy')"
                    $taggedBySystem = "$(Get-PropValue $c 'TaggedByRoleSystem')"

                    # Cache Azure scope reasoning once for each assignment scope.
                    if ($roleSystem -eq 'Azure' -and $scopeReasoningDetails.Count -gt 0 -and -not [string]::IsNullOrEmpty("$scopeId") -and -not $scopeReasoningByScope.Contains("$scopeId")) {
                        $scopeReasoning = [System.Collections.Generic.List[object]]::new()
                        foreach ($sr in (Find-EntraOpsAzureScopeReasoning -ScopeReasoningDetails $scopeReasoningDetails -ScopeId "$scopeId")) {
                            $scopeReasoning.Add([ordered]@{
                                    resourceName            = "$(Get-PropValue $sr 'ScopeName')"
                                    resourceId              = "$(Get-PropValue $sr 'ScopeId')"
                                    eamTier                 = "$(Get-PropValue $sr 'EAMTier')"
                                    resultingScope          = "$(Get-PropValue $sr 'ResultingScope')"
                                    reason                  = "$(Get-PropValue $sr 'Reason')"
                                    managedIdentityObjectId = "$(Get-PropValue $sr 'ManagedIdentityObjectId')"
                                    # ScopeRelation identifies the scope reason relative to the assignment scope.
                                    source                  = "$(Get-PropValue $sr 'Source')"
                                    scopeRelation           = $(if ("$(Get-PropValue $sr 'ScopeId')".TrimEnd('/') -ieq "$scopeId".TrimEnd('/')) { 'Exact' } else { 'Descendant resource' })
                                })
                        }
                        if ($scopeReasoning.Count -gt 0) {
                            $scopeReasoningByScope["$scopeId"] = $scopeReasoning
                        } else {
                            # Cache scope IDs without matching reasoning entries.
                            $scopeReasoningByScope["$scopeId"] = @()
                        }
                    }

                    $paths.Add([ordered]@{
                            objectId           = $objectId
                            objectName         = $display
                            objectType         = $objType
                            objectTier         = $objTier
                            system             = $roleSystem
                            role               = $roleName
                            roleType           = $roleType
                            roleIsPrivileged   = $isPriv
                            service            = $service
                            serviceTier        = $svcTier
                            # Assignment detail.
                            assignmentInstanceId = $assignInstanceId
                            assignmentId       = $assignId
                            scopeName          = $scopeName
                            scopeId            = $scopeId
                            assignmentType     = $assignType
                            assignmentSubType  = $assignSub
                            pimManaged         = $pimManaged
                            pimAssignmentType  = $pimType
                            transitiveBy       = $transitiveBy
                            taggedBy           = $taggedBy
                            taggedByRoleSystem = $taggedBySystem
                            # Breach: object less privileged than the service it reaches.
                            breach             = ($objTier -gt $svcTier)
                            tier0Breach        = ($objTier -gt 0 -and $svcTier -eq 0)
                            # Scope reasoning is resolved through scopeReasoningByScope.
                        })
                }
            }
        }

        Write-Verbose "[$fileNumber/$($files.Count)] Finished ${roleSystem}: processed $sourceObjectCount source object(s), added $($paths.Count - $pathsBeforeFile) assignment path(s)."
    }

    $repoRootFull = (Resolve-Path -LiteralPath $RepoRoot).Path
    $tenantName = Get-EntraOpsReportingTenantName -RepoRoot $RepoRoot
    $payload = [ordered]@{
        tenantName    = $tenantName
        generatedFrom = @(@($files) | ForEach-Object {
                if ($_.FullName.StartsWith($repoRootFull)) {
                    $_.FullName.Substring($repoRootFull.Length).TrimStart('\', '/') -replace '\\', '/'
                } else {
                    $_.Name
                }
            })
        tierLabels    = $TierLabels
        objects       = $objects
        paths         = $paths
        # Scope-reasoning entries shared by assignment scope ID.
        scopeReasoningByScope = $scopeReasoningByScope
    }

    $json = $payload | ConvertTo-Json -Depth 10 -Compress
    $content = "// Auto-generated by New-EntraOpsTierBreachAnalyzerData - do not edit by hand.`n" +
    "window.ENTRAOPS_TB_DATA = $json;`n"

    Write-Verbose "Built Tier Breach Analyzer payload with $($objects.Count) object record(s) and $($paths.Count) assignment path(s)."

    if ($PSCmdlet.ShouldProcess($OutFile, 'Write Tier Breach Analyzer dataset')) {
        Save-EntraOpsReportDataFile -Content $content -LiteralPath $OutFile

        $nBreach = @($paths | Where-Object { $_.breach }).Count
        $nTier0 = @($paths | Where-Object { $_.tier0Breach }).Count
        Write-Host "Objects (user/sp): $($objects.Count)"
        Write-Host "Assignment paths: $($paths.Count)"
        Write-Host "Tier breaches: $nBreach"
        Write-Host "Tier 0 breaches: $nTier0"
        Write-Host "Wrote $OutFile"
    }

    if ($PassThru) { $payload }
}

# SIG # Begin signature block
# MIIniAYJKoZIhvcNAQcCoIIneTCCJ3UCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBrO10qJdhxDx/u
# jLlV8CDdOyxTt7oyUer8PwjMQB28haCCIDwwggXJMIIEsaADAgECAhAbtY8lKt8j
# 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
# ARUwLwYJKoZIhvcNAQkEMSIEIMN98/gekOSkthPMGYF9S2xy35ueTILeIsiavW3w
# ixvLMA0GCSqGSIb3DQEBAQUABIIBgBUFDn8bD97O6CeLiaXaET7xN+fh558DW3N6
# tEipdK+d7ckWCDAe+mQWV3CZ1Ue/RjylgJW0lz2v+JowD3CHVhxDBKgMwPkQyZBV
# rFQpBmNeO8c6Kah7JigpfIdyM3fbReyCrZ45D0xED1q9+i3SPb+4HF3UDm9XKqY9
# Js2KNCf/ucfJQesrqA6SRsfiESx8NYPKfYwcu2UsYi3ct6Ma2w/KQDxKJw28kvv0
# +VxQLAoI83ytO2HtemCS5+JXwDIjW06NROQyWlEaapKALpxXEyLjF9AAv7rnRIu3
# F6eo32JSsLUXFuPCr0tVzQH8/HjnW/Dh3FHTkRf539lbF+wgpBZMKbNM1l+uk+So
# 9ZXRos78vlHRm+APiTj4CuiGUl/omiwo+xmHC0X1vaA3XC97d2nWjQDoNsGXej4n
# WlCanjYVkXBC+4OGm68+dsqKjNPdFSo5WRBCz6Q4CXcVqjTgbxJRwonSTmEUdVxo
# XoBzWYLSnRcAY3e9edblgdck2mzhk6GCBAIwggP+BgkqhkiG9w0BCQYxggPvMIID
# 6wIBATBqMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0
# ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQQIQ
# KPB3wRw2vf5fdDJHcCcuAzANBglghkgBZQMEAgIFAKCCAVYwGgYJKoZIhvcNAQkD
# MQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNjEwMTMwOTQxNTdaMDcG
# CyqGSIb3DQEJEAIvMSgwJjAkMCIEIIW+kOEK0kONfMkotq9IsJqyCBd87PiwEmxY
# 05EFJcQ8MD8GCSqGSIb3DQEJBDEyBDBB49tDr0uF5A/JadBZGyLqAZ2OD4ZYkCZj
# irFDdBB/UXDzQGa7KQeB8p1IQF6PdPwwgZ8GCyqGSIb3DQEJEAIMMYGPMIGMMIGJ
# MIGGBBRXFGhBDKha80JO+RZKUTYQ9NONmDBuMFqkWDBWMQswCQYDVQQGEwJQTDEh
# MB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQwIgYDVQQDExtDZXJ0
# dW0gVGltZXN0YW1waW5nIDIwMjEgQ0ECECjwd8EcNr3+X3QyR3AnLgMwDQYJKoZI
# hvcNAQEBBQAEggIAJg2sKHVwCAgimFvtLkPIPp2KSzf/sVHQ+MykY5yZjihGWktJ
# phM9lfOfh5cMXy6Xn0a/B+3zTlHxxubpBBqMudq3Kz/vHrEd0bju8M2rYWcRVD+r
# Bvwd5WJaq9Rv3Qx6A6W4nNYctESyDNV3k4+ypi1ANbV0RoDSFXaRWYmE5Y9qW0s5
# SHOJA367VKGjq1lLGYPC2mz5nzEUC7iro4lPfGMbNSOCz4MVmdbg1giYk2c4YZyR
# ZjE64TH1jZABwSTEmjfWJDQ4MWWGtmgIuoOYwD5SddN8cqaYluqKhuhbAEtIJow4
# 6vxCmRbTWGXN1esdP4pa1RlkYxbBXY5g+e+tACX9lcgJYyT2GzCUm5ezURM63EAL
# 81CcgRiI+i8vPVfHJ3pcICQP3D3tpcsiOqQ/mPCVazZv70aT503fmxNprVHwm7zU
# xwqLzxG4CgjwGFpR8udulYifYX8UuSKndKq6I8mOpSqS8+a4e5fp1rA0qhrXhhNv
# bNiBesYoJPkwjk7h7LDrAbPJ0eMYyocfPB5EAk8/J6+5BNCgatGYJb53Q+hX54Bj
# EmNoqG+/0+QhzFbCdK/Hs603jOVy1IdYwFw5XyW8cjNfOBRyFbJxqIRHJeaPFvv1
# vGmzzvkHACwCVY0UZHxek1dYQEScgJkA0PD2jVpoJA1x4t4B3T3VOCP+ezY=
# SIG # End signature block