PKIStats.psm1


# Déclare la table une seule fois pour le module entier
    $script:TplByOid = @{}
#region function_parse
function Update-Top5 {
    [CmdletBinding()]
    param (
        [Parameter()]
        [array]$GlobalTop5 = @(),

        [Parameter(Mandatory = $true)]
        [hashtable]$LocalStats
    )

    # Combine current global top 5 and local stats into a single array
    $combined = @()

    foreach ($item in $GlobalTop5) {
        $combined += [pscustomobject]@{
            Name  = $item.Name
            Count = $item.Count
        }
    }

    foreach ($entry in $LocalStats.GetEnumerator()) {
        $combined += [pscustomobject]@{
            Name  = $entry.Key
            Count = $entry.Value
        }
    }

    # Merge duplicates by name and sum their counts
    $merged = @{}
    foreach ($obj in $combined) {
        if ($merged.ContainsKey($obj.Name)) {
            $merged[$obj.Name] += $obj.Count
        } else {
            $merged[$obj.Name] = $obj.Count
        }
    }

    # Return a new top 5 list sorted by count descending
    $result = $merged.GetEnumerator() | ForEach-Object {
        [pscustomobject]@{
            Name  = $_.Key
            Count = $_.Value
        }
    } | Sort-Object Count -Descending | Select-Object -First 5

    return $result
}
   function Get-Top5 {
    param(
        [Parameter(Mandatory)][object[]]$Data
    )

    $firstProp = $Data[0].PSObject.Properties.Name | Where-Object { $_ -ne 'Count' } | Select-Object -First 1
    
    if (-not $firstProp) { return @() }

    $Data | Group-Object $firstProp |
        ForEach-Object { [pscustomobject]@{ $firstProp = $_.Name; Count = ($_.Group | Measure-Object Count -Sum).Sum } } |  Sort-Object Count -Descending | Select-Object -First 5
        
}
   function Get-Top5FromIssued {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [array]$InputObject,

        [Parameter(Mandatory)]
        [string]$FieldName,  # ex : "Request.RequesterName"

        [Parameter(Mandatory)]
        [string]$Label        # ex : "Requester" ou "Target"
    )

    $top = $InputObject |
        Group-Object { $_.Properties[$FieldName]  } |
        Where-Object { $_.Count -ge 2 } |
        Sort-Object Count -Descending |
        Select-Object -First 5 |
        ForEach-Object {
            $name = if ([string]::IsNullOrWhiteSpace($_.Name)) { "Unknown" } else { $_.Name }
            [pscustomobject]@{
                "$Label" = $name
                Count    = $_.Count
            }
        }

    return $top
}
   function Resolve-TemplateName {
    param($row)

    if (-not $script:TplByOid -or $script:TplByOid.Count -eq 0) {
        try {
            Get-CertificateTemplate | ForEach-Object {
                if ($_.Oid.Value) {
                    $script:TplByOid[$_.Oid.Value] = $_.Name
                }
            }
        } catch {
            Write-Warning "Failed to load certificate templates: $_"
        }
    }

    $raw = if ($row.CertificateTemplate) { $row.CertificateTemplate } else { $row.CertificateTemplateOid }

    if ($raw -match '^\d+(\.\d+)+') {
        if ($script:TplByOid.ContainsKey($raw)) {
            return $script:TplByOid[$raw]
        }
    }

    return $raw
}
   function Format-Cert {
        param(
            [Parameter(Mandatory)]$Row,
            [Parameter(Mandatory)][datetime]$Now
        )
        $dn = $Row.Properties["Request.DistinguishedName"]
        $subjectCN = if ($dn -match "CN=([^,]+)") { $matches[1] } else { $dn }

        $days = [int]($Row.NotAfter - $Now).TotalDays

        [pscustomobject]@{
            RequestID    = $Row.RequestID
            Template     = Resolve-TemplateName $Row
            Name         = $subjectCN
            Request      = $Row.Properties["Request.RequesterName"]
            Subject      = $Row.CommonName
            Created      = $Row.NotBefore
            ExpiresOn    = $Row.NotAfter
            DaysLeft     = $days
            ConfigString = $Row.ConfigString
            Table        = $Row.Table
        }
    }
   function Get-CSRSummary {
    param (
        $RawRequest
    )

    $result = [ordered]@{
        ComboKey    = $null
        ProcessName = $null
    }

    try {
        $bytes = [Convert]::FromBase64String($RawRequest)
        $req = New-Object SysadminsLV.PKI.Cryptography.X509Certificates.X509CertificateRequest -ArgumentList (, $bytes)

        # Signature + public key
        $sigAlgo = $req.SignatureAlgorithm.FriendlyName
        $keyAlgo = $req.PublicKey.Oid.FriendlyName
        $keyLen  = $req.PublicKey.Key.KeySize

        $result.ComboKey = "$sigAlgo - $keyAlgo - ${keyLen}bits"

        # Extraire ProcessName à partir du texte formaté
        if ($req.Attributes) {
        $req.Attributes | ForEach-Object {

                if($_.Oid.Value -eq "1.3.6.1.4.1.311.21.20") {

        $asn = [SysadminsLV.Asn1Parser.Asn1Reader]::new($_.RawData)

        if (($asn.Tag -eq 48) -and $asn.MoveNext() -and ($asn.Tag -eq 2) -and $asn.MoveNext() -and ($asn.Tag -eq 12)) {
            $bytes = $asn.GetPayload()
            $encoding = [System.Text.UnicodeEncoding]::ASCII
            if ($bytes -cmatch '[^\x20-\x7F]') {
                $encoding = [System.Text.UnicodeEncoding]::Unicode
            }

            #$result.MachineName = $encoding.GetString($asn.GetPayload())
            $null = $asn.MoveNext()
            #$result.UserName = $encoding.GetString($asn.GetPayload())
            $null = $asn.MoveNext()
            $result.ProcessName = $encoding.GetString($asn.GetPayload())
            #$result.RequestType = $CertRequest.RequestType
            #$result.ReqID = $ReqID

    }
    }
    }
    }

    } catch {
        # No result
    }

    return [pscustomobject]$result
}
   function ConvertFrom-CSRExtensionMeta {
    param(
        $RawData
        )

    $result = @{
        ProcessName  = $null
    }

    $RawRequestBytes = [Convert]::FromBase64String($RawData)

    Try {
    $CertRequest = New-Object SysadminsLV.PKI.Cryptography.X509Certificates.X509CertificateRequest (,$RawRequestBytes)

    } catch {
             }

    if ($CertRequest.Attributes) {
    $CertRequest.Attributes | ForEach-Object {

                if($_.Oid.Value -eq "1.3.6.1.4.1.311.21.20") {

        $asn = [SysadminsLV.Asn1Parser.Asn1Reader]::new($_.RawData)

        if (($asn.Tag -eq 48) -and $asn.MoveNext() -and ($asn.Tag -eq 2) -and $asn.MoveNext() -and ($asn.Tag -eq 12)) {
            $bytes = $asn.GetPayload()
            $encoding = [System.Text.UnicodeEncoding]::ASCII
            if ($bytes -cmatch '[^\x20-\x7F]') {
                $encoding = [System.Text.UnicodeEncoding]::Unicode
            }

            $null = $asn.MoveNext()
            $null = $asn.MoveNext()
            $result.ProcessName = $encoding.GetString($asn.GetPayload())

    }
    }
    }
    }
    
    if ($result) {
    return [pscustomobject]$result
    }
}
   function Get-CertSignatureKeyCombo {
    param (
        $RawCertificate
    )

    try {
        $bytes = [Convert]::FromBase64String($RawCertificate)
        $certif = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($bytes)

        $sigAlgo = $certif.SignatureAlgorithm.FriendlyName
        $keyAlgo = $certif.PublicKey.Oid.FriendlyName
        $keyLen  = $certif.PublicKey.Key.KeySize

        $certif.Dispose()
        return "$sigAlgo - $keyAlgo - ${keyLen}bits"
    } catch {
        Write-Warning "Raw certificate error: $($_.Exception.Message)"
        return "Unknown"
    }
}
   function Update-StatCounter {
    [CmdletBinding()]
    param(
        [hashtable]$Bucket,
        [string]$Key
    )
    if ([string]::IsNullOrEmpty($Key)) { return }
    if ($Bucket.ContainsKey($Key)) {
        $Bucket[$Key]++
    } else {
        $Bucket[$Key] = 1
    }
}
   function Get-RandomColor {
    return "#{0:X6}" -f (Get-Random -Minimum 0 -Maximum 0xFFFFFF)
   }
   function Get-RevokerFromMessage {
    param([string]$Message)
    if ([string]::IsNullOrWhiteSpace($Message)) {
        return $null
    }
    return ($Message -split '\s+')[-1].Trim()
}
   function Resolve-StatusCode {
    param (
        [Parameter(Mandatory)]
        [int]$Code
    )

    $statusMap = @{
    0x80094800 = 'CERTSRV_E_INVALID_REQUEST'
    0x80094801 = 'CERTSRV_E_INVALID_CA_CERTIFICATE'
    0x80094802 = 'CERTSRV_E_UNSUPPORTED_CERT_TYPE'
    0x80094803 = 'CERTSRV_E_NO_CERT_TYPE'
    0x80094812 = 'CERTSRV_E_SUBJECT_ALT_NAME_REQUIRED'
    0x80094814 = 'CERTSRV_E_INVALID_EK'
    0x80094001 = 'CERTSRV_E_BAD_REQUESTSUBJECT'
    0x80094002 = 'CERTSRV_E_NO_REQUEST'
    0x80094003 = 'CERTSRV_E_BAD_REQUESTSTATUS'
    0x80094004 = 'CERTSRV_E_PROPERTY_EMPTY'
    0x80094005 = 'CERTSRV_E_INVALID_PROPERTY'
    0x80094006 = 'CERTSRV_E_INVALID_EXTENSION'
    0x80092004 = 'CRYPT_E_NOT_FOUND'
    0x80092009 = 'CRYPT_E_BAD_MSG'
    0x80092010 = 'CRYPT_E_BAD_EXTENSIONS'
    0x80092013 = 'CRYPT_E_REVOCATION_OFFLINE'
    0x80092022 = 'CRYPT_E_ISSUER_SERIALNUMBER'
    0x80070005 = 'E_ACCESSDENIED'
    0x80070057 = 'E_INVALIDARG'
    }

    $hexCode = '0x{0:X8}' -f ($Code -band 0xFFFFFFFF)
    
    if ($statusMap.ContainsKey([int]$hexCode)) {
        return "$hexCode : $($statusMap[[int]$hexCode])"
    } else {
        return $hexCode
    }
}
   function Format-FailedRequest {
    param (
        [Parameter(Mandatory)]
        $cert
    )

    return [pscustomobject]@{
        RequestID     = $cert.RequestID
        StatusCode    = ('0x{0:X8}' -f ($cert.'Request.StatusCode'))
        Requester     = $cert.'Request.RequesterName'
        CommonName    = $cert.'Request.CommonName'
        Template      = Resolve-TemplateName -row $cert
        Submitted     = $cert.'Request.SubmittedWhen'
        Resolved      = $cert.'Request.ResolvedWhen'
        Config        = $cert.ConfigString
        Message       = $cert.'Request.DispositionMessage'
    }
}
#endregion function_parse

#region Displa_Functions
    function Show-ConsoleStatus {
    param (
        [string]$Label = "Issued",
        [int]$Page,
        [int]$Count,
        [TimeSpan]$Elapsed
    )

    $line = "[$Label] > Page $Page processed ($Count items) | Elapsed: $($Elapsed.ToString("hh\:mm\:ss"))"

    try {
        $oldColor = [Console]::ForegroundColor
        [Console]::ForegroundColor = 'Green'

        [Console]::SetCursorPosition(0, [Console]::CursorTop)
        [Console]::Write($line.PadRight([Console]::WindowWidth))

        [Console]::ForegroundColor = $oldColor
    } catch {
        Write-Host $line -ForegroundColor Orange
    }
}
    function Get-CAState {
    param([datetime]$ExpiryDate)

    if (-not $ExpiryDate) {
        return 'Unknown'
    }

    if ($ExpiryDate -gt (Get-Date).AddDays(90)) {
        return 'Active'
    }
    elseif ($ExpiryDate -gt (Get-Date)) {
        return 'Warning'
    }
    else {
        return 'Expired'
    }
}
    function Get-CRLStatusFromAD {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [array]$CertificationAuthority
    )

    # list CRL in AD without RSAT AD
    $configNC = ([ADSI]"LDAP://RootDSE").configurationNamingContext
    $searchBase = "CN=CDP,CN=Public Key Services,CN=Services,$configNC"

    $CRLCombined = @()
    $crlObjects  = @()

    $searcher = New-Object System.DirectoryServices.DirectorySearcher
    $searcher.SearchRoot = "LDAP://$searchBase"
    $searcher.Filter = "(objectClass=cRLDistributionPoint)"
    $searcher.PageSize = 1000
    $searcher.PropertiesToLoad.Add("whenCreated") | Out-Null
    $searcher.PropertiesToLoad.Add("whenChanged") | Out-Null
    $searcher.PropertiesToLoad.Add("name")        | Out-Null

    $results = $searcher.FindAll()
    foreach ($result in $results) {
    $entry = $result.Properties

    $crlObjects += [PSCustomObject]@{
        DistinguishedName = $result.Path -replace "^LDAP://", ""
        Name              = $entry["name"] | Select-Object -First 1
        Created           = $entry["whencreated"] | Select-Object -First 1
        Changed           = $entry["whenchanged"] | Select-Object -First 1
    }
}
    foreach ($ca in Get-CRLValidityPeriod -CertificationAuthority $CertificationAuthority) {
        $caName = $ca.DisplayName
        $crlAD  = $crlObjects | Where-Object { $_.Name -eq $caName }

        $CRLCombined += [pscustomobject]@{
            Name         = $ca.Name
            DisplayName  = $ca.DisplayName
            ComputerName = $ca.ComputerName
            BaseCRL      = $ca.BaseCRL
            DeltaCRL     = $ca.DeltaCRL
            Modified     = $ca.IsModified
            Created      = if ($crlAD) { $crlAD.Created.ToString("yyyy-MM-dd") } else { $null }
            LastUpdate   = if ($crlAD) { $crlAD.Changed.ToString("yyyy-MM-dd") } else { $null }
            ADStatus     = if ($crlAD) { "Published" } else { "Not Published" }
        }
    }

    return $CRLCombined
}
    function Get-PKIEnrollmentServices {
    param (
        [string]$SearchBase = ([ADSI]"LDAP://RootDSE").configurationNamingContext
    )

    $searcher = New-Object System.DirectoryServices.DirectorySearcher
    $searcher.SearchRoot = "LDAP://$SearchBase"
    $searcher.Filter = "(objectClass=pKIEnrollmentService)"
    $searcher.PageSize = 100
    $searcher.PropertiesToLoad.Add("dNSHostName") | Out-Null
    $searcher.PropertiesToLoad.Add("displayName") | Out-Null
    $searcher.PropertiesToLoad.Add("whenCreated") | Out-Null
    $searcher.PropertiesToLoad.Add("Name") | Out-Null

    $results = $searcher.FindAll()

    foreach ($result in $results) {
        $entry = $result.Properties
        [PSCustomObject]@{
            DistinguishedName = $result.Path -replace "^LDAP://", ""
            dNSHostName       = $entry["dnshostname"] | Select-Object -First 1
            DisplayName       = $entry["displayname"] | Select-Object -First 1
            Created           = $entry["whencreated"] | Select-Object -First 1
            Name              = $entry["Name"] | Select-Object -First 1
        }
    }
}
#endregion Displa_Functions
# SIG # Begin signature block
# MIImkwYJKoZIhvcNAQcCoIImhDCCJoACAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCiFq+EZlGEH6jP
# 9ObLwl3yGZ/NR1TqVey7FNCdE+yPFKCCICMwggWNMIIEdaADAgECAhAOmxiO+dAt
# 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV
# BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa
# Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy
# dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD
# ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
# ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E
# MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy
# unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF
# xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1
# 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB
# MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR
# WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6
# nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB
# YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S
# UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x
# q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB
# NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP
# TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC
# AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp
# Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv
# bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0
# aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB
# LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc
# Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov
# Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy
# oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW
# juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF
# mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z
# twGpn1eqXijiuZQwggYoMIIEEKADAgECAhBrxlWg9go45bxtH9Zi+WCgMA0GCSqG
# SIb3DQEBCwUAMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBT
# eXN0ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBDb2RlIFNpZ25pbmcgMjAyMSBD
# QTAeFw0yNDExMDYwOTQ0MjlaFw0yNTExMDYwOTQ0MjhaME4xCzAJBgNVBAYTAkZS
# MQ8wDQYDVQQHDAZUb3Vsb24xFjAUBgNVBAoMDU1laGRpIERha2hhbWExFjAUBgNV
# BAMMDU1laGRpIERha2hhbWEwggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIB
# gQCsFc3e5PwEJuycVRR54Qp8hFEckVwj7u1hMc7fejXKC/oR+uixlujLAHA9NcGX
# jcQIXNP3GmezLF3Tj6Jvcs/kNT/a5zqjI5HEfIap7EHwf03f5060+Rc21v1UDjzj
# DZzi9xFFum8eeGLc4pTzUB3wP3+M+mY7d5QlTjIxZSNnMBisJE8ASqG9JtRcQmIz
# HACI70xRCQVV8ZjJ8J+Shr6wkNdDy/IjR+Y9VkMRIJozWR+pqbKuQOIDBSxQYVHg
# bT+gsLOfvHkBPJN0ZQe7eJdG7J78Z1nzNH9yXhZ0HHdPB80tUwM0HC1n4LO3kki/
# IBmg4Qq/UyMMQd826fJk3ylbAlf8w7N80INQcLLBGVECmWI21d9f3l5usvWDa+mJ
# ma57c6GUDY05Jg5owLgNREZsyRt5rOlg68NLmz9tuEkJA1D4ntpKq0KZc3HJv04x
# XTcfTEqbKYr7vZ//ENsell5UdUQxL6rGJzazhsK02ZcmasICiHNLfG/tBaolCbeM
# 8ekCAwEAAaOCAXgwggF0MAwGA1UdEwEB/wQCMAAwPQYDVR0fBDYwNDAyoDCgLoYs
# aHR0cDovL2Njc2NhMjAyMS5jcmwuY2VydHVtLnBsL2Njc2NhMjAyMS5jcmwwcwYI
# KwYBBQUHAQEEZzBlMCwGCCsGAQUFBzABhiBodHRwOi8vY2NzY2EyMDIxLm9jc3At
# Y2VydHVtLmNvbTA1BggrBgEFBQcwAoYpaHR0cDovL3JlcG9zaXRvcnkuY2VydHVt
# LnBsL2Njc2NhMjAyMS5jZXIwHwYDVR0jBBgwFoAU3XRdTADbe5+gdMqxbvc8wDLA
# cM0wHQYDVR0OBBYEFAG3sIcT8bRm7QyFu8699Gpkr5NmMEsGA1UdIAREMEIwCAYG
# Z4EMAQQBMDYGCyqEaAGG9ncCBQEEMCcwJQYIKwYBBQUHAgEWGWh0dHBzOi8vd3d3
# LmNlcnR1bS5wbC9DUFMwEwYDVR0lBAwwCgYIKwYBBQUHAwMwDgYDVR0PAQH/BAQD
# AgeAMA0GCSqGSIb3DQEBCwUAA4ICAQCJ58BnchFNGzLksJ9oHFEWTs643G+PKOHr
# 9RmrKSB/4MtPriG5iez+MFsGqYwkYd5QzqOIYg24ctfbWXJWG8Xj+YMfp1r+hkYq
# O0Abpv26sZ1ZjNGgGUbb3z7KqhY+IdVpZf2aG/Rycl5dE2LbhWqp9h24WfQCIS/e
# XxH7HmM9SEBHYbfOqlEA+RF/gRGYCQOg0ui2j0ZzIOrQGj3Njn/5rzP9OmPmLt4h
# DsixjFWgu598XmRKj5KW1MShFIjUuUzSmOWDgKA16lJi6LggdFAB/MImiDH48v8N
# /9R9En24pUGGj2XOgBX5SZ4kj+VN1YaY1vYPFp3wLu85zpgRZgZQC+WurX8s1tRn
# iCIj/+ajUB4G4TcbTz6k16X1Yz9ba1y7p/hJB92uDW7esMGgqzEv+Osd11bVoNmv
# CE8Twsz0cuFJqBtVZIycCkgw/AVyJIsNS6RADi94PvbOf8rty8HV3bHmm6O4wJVc
# 5ch50bL7JVyYTPN5OTzXSDx62wKi5ePZvEF7RX3cQlTQMYticde91khs2n2FZ06K
# Uin5DtQgxy0Q1ufFIDZthsk5AaSWiZzFgAgJt8JaQGPyGAYl2Sr8a/gMLpcBsPwI
# zdlDUOJwyHPxlR9ZiraUzF/1SSN7CgjqFSDAAZ+i4i8gZsPpU38GtBSLrw/CrnUB
# /KGcFNMvszCCBrQwggScoAMCAQICEA3HrFcF/yGZLkBDIgw6SYYwDQYJKoZIhvcN
# AQELBQAwYjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcG
# A1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgVHJ1c3Rl
# ZCBSb290IEc0MB4XDTI1MDUwNzAwMDAwMFoXDTM4MDExNDIzNTk1OVowaTELMAkG
# A1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdp
# Q2VydCBUcnVzdGVkIEc0IFRpbWVTdGFtcGluZyBSU0E0MDk2IFNIQTI1NiAyMDI1
# IENBMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALR4MdMKmEFyvjxG
# wBysddujRmh0tFEXnU2tjQ2UtZmWgyxU7UNqEY81FzJsQqr5G7A6c+Gh/qm8Xi4a
# PCOo2N8S9SLrC6Kbltqn7SWCWgzbNfiR+2fkHUiljNOqnIVD/gG3SYDEAd4dg2dD
# GpeZGKe+42DFUF0mR/vtLa4+gKPsYfwEu7EEbkC9+0F2w4QJLVSTEG8yAR2CQWIM
# 1iI5PHg62IVwxKSpO0XaF9DPfNBKS7Zazch8NF5vp7eaZ2CVNxpqumzTCNSOxm+S
# AWSuIr21Qomb+zzQWKhxKTVVgtmUPAW35xUUFREmDrMxSNlr/NsJyUXzdtFUUt4a
# S4CEeIY8y9IaaGBpPNXKFifinT7zL2gdFpBP9qh8SdLnEut/GcalNeJQ55IuwnKC
# gs+nrpuQNfVmUB5KlCX3ZA4x5HHKS+rqBvKWxdCyQEEGcbLe1b8Aw4wJkhU1JrPs
# FfxW1gaou30yZ46t4Y9F20HHfIY4/6vHespYMQmUiote8ladjS/nJ0+k6Mvqzfpz
# PDOy5y6gqztiT96Fv/9bH7mQyogxG9QEPHrPV6/7umw052AkyiLA6tQbZl1KhBtT
# asySkuJDpsZGKdlsjg4u70EwgWbVRSX1Wd4+zoFpp4Ra+MlKM2baoD6x0VR4RjSp
# WM8o5a6D8bpfm4CLKczsG7ZrIGNTAgMBAAGjggFdMIIBWTASBgNVHRMBAf8ECDAG
# AQH/AgEAMB0GA1UdDgQWBBTvb1NK6eQGfHrK4pBW9i/USezLTjAfBgNVHSMEGDAW
# gBTs1+OC0nFdZEzfLmc/57qYrhwPTzAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAww
# CgYIKwYBBQUHAwgwdwYIKwYBBQUHAQEEazBpMCQGCCsGAQUFBzABhhhodHRwOi8v
# b2NzcC5kaWdpY2VydC5jb20wQQYIKwYBBQUHMAKGNWh0dHA6Ly9jYWNlcnRzLmRp
# Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRSb290RzQuY3J0MEMGA1UdHwQ8MDow
# OKA2oDSGMmh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRS
# b290RzQuY3JsMCAGA1UdIAQZMBcwCAYGZ4EMAQQCMAsGCWCGSAGG/WwHATANBgkq
# hkiG9w0BAQsFAAOCAgEAF877FoAc/gc9EXZxML2+C8i1NKZ/zdCHxYgaMH9Pw5tc
# BnPw6O6FTGNpoV2V4wzSUGvI9NAzaoQk97frPBtIj+ZLzdp+yXdhOP4hCFATuNT+
# ReOPK0mCefSG+tXqGpYZ3essBS3q8nL2UwM+NMvEuBd/2vmdYxDCvwzJv2sRUoKE
# fJ+nN57mQfQXwcAEGCvRR2qKtntujB71WPYAgwPyWLKu6RnaID/B0ba2H3LUiwDR
# AXx1Neq9ydOal95CHfmTnM4I+ZI2rVQfjXQA1WSjjf4J2a7jLzWGNqNX+DF0SQzH
# U0pTi4dBwp9nEC8EAqoxW6q17r0z0noDjs6+BFo+z7bKSBwZXTRNivYuve3L2oiK
# NqetRHdqfMTCW/NmKLJ9M+MtucVGyOxiDf06VXxyKkOirv6o02OoXN4bFzK0vlNM
# svhlqgF2puE6FndlENSmE+9JGYxOGLS/D284NHNboDGcmWXfwXRy4kbu4QFhOm0x
# JuF2EZAOk5eCkhSxZON3rGlHqhpB/8MluDezooIs8CVnrpHMiD2wL40mm53+/j7t
# FaxYKIqL0Q4ssd8xHZnIn/7GELH3IdvG2XlM9q7WP/UwgOkw/HQtyRN62JK4S1C8
# uw3PdBunvAZapsiI5YKdvlarEvf8EA+8hcpSM9LHJmyrxaFtoza2zNaQ9k+5t1ww
# gga5MIIEoaADAgECAhEAmaOACiZVO2Wr3G6EprPqOTANBgkqhkiG9w0BAQwFADCB
# gDELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu
# QS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIG
# A1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMB4XDTIxMDUxOTA1MzIx
# OFoXDTM2MDUxODA1MzIxOFowVjELMAkGA1UEBhMCUEwxITAfBgNVBAoTGEFzc2Vj
# byBEYXRhIFN5c3RlbXMgUy5BLjEkMCIGA1UEAxMbQ2VydHVtIENvZGUgU2lnbmlu
# ZyAyMDIxIENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnSPPBDAj
# O8FGLOczcz5jXXp1ur5cTbq96y34vuTmflN4mSAfgLKTvggv24/rWiVGzGxT9YEA
# SVMw1Aj8ewTS4IndU8s7VS5+djSoMcbvIKck6+hI1shsylP4JyLvmxwLHtSworV9
# wmjhNd627h27a8RdrT1PH9ud0IF+njvMk2xqbNTIPsnWtw3E7DmDoUmDQiYi/ucJ
# 42fcHqBkbbxYDB7SYOouu9Tj1yHIohzuC8KNqfcYf7Z4/iZgkBJ+UFNDcc6zokZ2
# uJIxWgPWXMEmhu1gMXgv8aGUsRdaCtVD2bSlbfsq7BiqljjaCun+RJgTgFRCtsuA
# Ew0pG9+FA+yQN9n/kZtMLK+Wo837Q4QOZgYqVWQ4x6cM7/G0yswg1ElLlJj6NYKL
# w9EcBXE7TF3HybZtYvj9lDV2nT8mFSkcSkAExzd4prHwYjUXTeZIlVXqj+eaYqoM
# TpMrfh5MCAOIG5knN4Q/JHuurfTI5XDYO962WZayx7ACFf5ydJpoEowSP07YaBiQ
# 8nXpDkNrUA9g7qf/rCkKbWpQ5boufUnq1UiYPIAHlezf4muJqxqIns/kqld6JVX8
# cixbd6PzkDpwZo4SlADaCi2JSplKShBSND36E/ENVv8urPS0yOnpG4tIoBGxVCAR
# PCg1BnyMJ4rBJAcOSnAWd18Jx5n858JSqPECAwEAAaOCAVUwggFRMA8GA1UdEwEB
# /wQFMAMBAf8wHQYDVR0OBBYEFN10XUwA23ufoHTKsW73PMAywHDNMB8GA1UdIwQY
# MBaAFLahVDkCw6A/joq8+tT4HKbROg79MA4GA1UdDwEB/wQEAwIBBjATBgNVHSUE
# DDAKBggrBgEFBQcDAzAwBgNVHR8EKTAnMCWgI6Ahhh9odHRwOi8vY3JsLmNlcnR1
# bS5wbC9jdG5jYTIuY3JsMGwGCCsGAQUFBwEBBGAwXjAoBggrBgEFBQcwAYYcaHR0
# cDovL3N1YmNhLm9jc3AtY2VydHVtLmNvbTAyBggrBgEFBQcwAoYmaHR0cDovL3Jl
# cG9zaXRvcnkuY2VydHVtLnBsL2N0bmNhMi5jZXIwOQYDVR0gBDIwMDAuBgRVHSAA
# MCYwJAYIKwYBBQUHAgEWGGh0dHA6Ly93d3cuY2VydHVtLnBsL0NQUzANBgkqhkiG
# 9w0BAQwFAAOCAgEAdYhYD+WPUCiaU58Q7EP89DttyZqGYn2XRDhJkL6P+/T0IPZy
# xfxiXumYlARMgwRzLRUStJl490L94C9LGF3vjzzH8Jq3iR74BRlkO18J3zIdmCKQ
# a5LyZ48IfICJTZVJeChDUyuQy6rGDxLUUAsO0eqeLNhLVsgw6/zOfImNlARKn1FP
# 7o0fTbj8ipNGxHBIutiRsWrhWM2f8pXdd3x2mbJCKKtl2s42g9KUJHEIiLni9Byo
# qIUul4GblLQigO0ugh7bWRLDm0CdY9rNLqyA3ahe8WlxVWkxyrQLjH8ItI17RdyS
# aYayX3PhRSC4Am1/7mATwZWwSD+B7eMcZNhpn8zJ+6MTyE6YoEBSRVrs0zFFIHUR
# 08Wk0ikSf+lIe5Iv6RY3/bFAEloMU+vUBfSouCReZwSLo8WdrDlPXtR0gicDnytO
# 7eZ5827NS2x7gCBibESYkOh1/w1tVxTpV2Na3PR7nxYVlPu1JPoRZCbH86gc96UT
# vuWiOruWmyOEMLOGGniR+x+zPF/2DaGgK2W1eEJfo2qyrBNPvF7wuAyQfiFXLwvW
# HamoYtPZo0LHuH8X3n9C+xN4YaNjt2ywzOr+tKyEVAotnyU9vyEVOaIYMk3IeBrm
# Fnn0gbKeTTyYeEEUz/Qwt4HOUBCrW602NCmvO1nm+/80nLy5r0AZvCQxaQ4wggbt
# MIIE1aADAgECAhAKgO8YS43xBYLRxHanlXRoMA0GCSqGSIb3DQEBCwUAMGkxCzAJ
# BgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGln
# aUNlcnQgVHJ1c3RlZCBHNCBUaW1lU3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYgMjAy
# NSBDQTEwHhcNMjUwNjA0MDAwMDAwWhcNMzYwOTAzMjM1OTU5WjBjMQswCQYDVQQG
# EwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0
# IFNIQTI1NiBSU0E0MDk2IFRpbWVzdGFtcCBSZXNwb25kZXIgMjAyNSAxMIICIjAN
# BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0EasLRLGntDqrmBWsytXum9R/4Zw
# CgHfyjfMGUIwYzKomd8U1nH7C8Dr0cVMF3BsfAFI54um8+dnxk36+jx0Tb+k+87H
# 9WPxNyFPJIDZHhAqlUPt281mHrBbZHqRK71Em3/hCGC5KyyneqiZ7syvFXJ9A72w
# zHpkBaMUNg7MOLxI6E9RaUueHTQKWXymOtRwJXcrcTTPPT2V1D/+cFllESviH8Yj
# oPFvZSjKs3SKO1QNUdFd2adw44wDcKgH+JRJE5Qg0NP3yiSyi5MxgU6cehGHr7zo
# u1znOM8odbkqoK+lJ25LCHBSai25CFyD23DZgPfDrJJJK77epTwMP6eKA0kWa3os
# Ae8fcpK40uhktzUd/Yk0xUvhDU6lvJukx7jphx40DQt82yepyekl4i0r8OEps/FN
# O4ahfvAk12hE5FVs9HVVWcO5J4dVmVzix4A77p3awLbr89A90/nWGjXMGn7FQhmS
# lIUDy9Z2hSgctaepZTd0ILIUbWuhKuAeNIeWrzHKYueMJtItnj2Q+aTyLLKLM0Mh
# eP/9w6CtjuuVHJOVoIJ/DtpJRE7Ce7vMRHoRon4CWIvuiNN1Lk9Y+xZ66lazs2kK
# FSTnnkrT3pXWETTJkhd76CIDBbTRofOsNyEhzZtCGmnQigpFHti58CSmvEyJcAlD
# VcKacJ+A9/z7eacCAwEAAaOCAZUwggGRMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYE
# FOQ7/PIx7f391/ORcWMZUEPPYYzoMB8GA1UdIwQYMBaAFO9vU0rp5AZ8esrikFb2
# L9RJ7MtOMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDCB
# lQYIKwYBBQUHAQEEgYgwgYUwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2lj
# ZXJ0LmNvbTBdBggrBgEFBQcwAoZRaHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29t
# L0RpZ2lDZXJ0VHJ1c3RlZEc0VGltZVN0YW1waW5nUlNBNDA5NlNIQTI1NjIwMjVD
# QTEuY3J0MF8GA1UdHwRYMFYwVKBSoFCGTmh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNv
# bS9EaWdpQ2VydFRydXN0ZWRHNFRpbWVTdGFtcGluZ1JTQTQwOTZTSEEyNTYyMDI1
# Q0ExLmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZI
# hvcNAQELBQADggIBAGUqrfEcJwS5rmBB7NEIRJ5jQHIh+OT2Ik/bNYulCrVvhREa
# fBYF0RkP2AGr181o2YWPoSHz9iZEN/FPsLSTwVQWo2H62yGBvg7ouCODwrx6ULj6
# hYKqdT8wv2UV+Kbz/3ImZlJ7YXwBD9R0oU62PtgxOao872bOySCILdBghQ/ZLcdC
# 8cbUUO75ZSpbh1oipOhcUT8lD8QAGB9lctZTTOJM3pHfKBAEcxQFoHlt2s9sXoxF
# izTeHihsQyfFg5fxUFEp7W42fNBVN4ueLaceRf9Cq9ec1v5iQMWTFQa0xNqItH3C
# PFTG7aEQJmmrJTV3Qhtfparz+BW60OiMEgV5GWoBy4RVPRwqxv7Mk0Sy4QHs7v9y
# 69NBqycz0BZwhB9WOfOu/CIJnzkQTwtSSpGGhLdjnQ4eBpjtP+XB3pQCtv4E5UCS
# Dag6+iX8MmB10nfldPF9SVD7weCC3yXZi/uuhqdwkgVxuiMFzGVFwYbQsiGnoa9F
# 5AaAyBjFBtXVLcKtapnMG3VH3EmAp/jsJ3FVF3+d1SVDTmjFjLbNFZUWMXuZyvgL
# fgyPehwJVxwC+UpX2MSey2ueIu9THFVkT+um1vshETaWyQo8gmBto/m3acaP9Qsu
# Lj3FNwFlTxq25+T4QwX9xa6ILs84ZPvmpovq90K8eWyG2N01c4IhSOxqt81nMYIF
# xjCCBcICAQEwajBWMQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEg
# U3lzdGVtcyBTLkEuMSQwIgYDVQQDExtDZXJ0dW0gQ29kZSBTaWduaW5nIDIwMjEg
# Q0ECEGvGVaD2CjjlvG0f1mL5YKAwDQYJYIZIAWUDBAIBBQCggYQwGAYKKwYBBAGC
# NwIBDDEKMAigAoAAoQKAADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor
# BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgHKuFZReR
# GZog2PTmBitWDhAFu0QD+LDlwOm1eEmcQgUwDQYJKoZIhvcNAQEBBQAEggGAj9yl
# yNJG7tceWvu41Y5QbPsG7tOUhVO2PBV1JX6aVsv9B6Mj2dLSZdJMLItKA5N5PlGE
# 5Ypz3pSPG6axfb/sBWfhcibc4QRTGvcmi4dFBiDk4G4m5b0HC2HI2Z2VxKuh3b4T
# FJ+if5B+VWI/cUDxy/rjhFgeuoPlc3JJwep6jlDvWHqulElLTq1h3AiF7L6q6UcB
# DslfWaE5Gy9Jutkt5hgGNhw3zNlmLeXYkVAk3ipqvw38K4zH8INjy6hrsBYpoe2k
# oYYKoDrxeeInaIuwhwPGIMIUmJecqjCgM+YkJcw5mbM0S3mSFxk26A9vn+CtF6Dm
# TX8XH7M2dACwbM3E4r9de9x5TluCjoIDPE7aCZ2tK9Zy+RG1VibEwGHRiCSyEn+C
# wPNgfCpiI/wt3FSlqrJfkCgHBvUTjyWTOhF/1zcdvBGioIlsz2m8rRLIXXKEc0Gv
# fNZc15fuzA7QUL6bOBWGT3Mpm8yMfz5cuM/fSVZEsnrDbW5VwJJFyYeHcD3ZoYID
# JjCCAyIGCSqGSIb3DQEJBjGCAxMwggMPAgEBMH0waTELMAkGA1UEBhMCVVMxFzAV
# BgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVk
# IEc0IFRpbWVTdGFtcGluZyBSU0E0MDk2IFNIQTI1NiAyMDI1IENBMQIQCoDvGEuN
# 8QWC0cR2p5V0aDANBglghkgBZQMEAgEFAKBpMBgGCSqGSIb3DQEJAzELBgkqhkiG
# 9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTI1MTAwMzIwMjkxMFowLwYJKoZIhvcNAQkE
# MSIEIMRNa0clslPQfnIqTzJJVybQ92ULiSn6GGGMSAVe1S73MA0GCSqGSIb3DQEB
# AQUABIICAG0nbsF9SY0nYIYOI/UC1/LappzRoahyR0Mr2i+kMxPfW/loX/xxepiR
# ICZG1cZevisEialgYcBfVWP261vga3n4A+fsxF4y32mCzaMedjhJARX+g4pAz8Cq
# dqB+afpD3phqQDnxWwG+qRC9YDmgyLDGqBG8cNZaslnIxArXDAu8buh77loFYt+z
# H0zf0VIKea/6CgLY+coshHcHQP9xctNlCWbAUNqnhi5FPXWm1qALlcswdNo4vtkM
# rH97yguqEtCLoDox/SQCgMyVxFO7vNSI4ChxEa0spmuZrFGF/fwabSS65ESpk9zn
# 2dvNnZiWhICyB45bu3AX3bTeDYSJLNRiLRnbqvK4POx7PUvitiQ9+1kGA3U/9AcS
# /fOOlA9QaTdXSLs5exD227vZomRqMcU7izJGQz9qVgycblysxpf6HGiI9KbiJ15v
# db1wj0G6sEgn1ivJiuHtilWrC1a8qQrmNyhV/cgjDk3j260uxZbhXJnSJ/jWSf0R
# 95/xtCtXKeoXxCJqjUDGQGPBBTpWDznXZE3YMA3yS0ZVPkvwBmIs9s1b1lM9UHuc
# Wh3Z/igCYqvGQpl2cY5sKakgmfIdkbIS86s97FbXOlN9HCJN4w0TN9O8gmtPTdOO
# y0xqaPbcYo0mhTywCZBNq3Khq9RWqggBvsol0+lSnM59tSa0mQ2e
# SIG # End signature block