EPM/Agents.ps1

class EpmAgentInfo {
    [string]$AgentUid
    [string]$MachineName
    [string]$Deployment
    [string]$Disabled
    [string]$Created
    [string]$Modified
}

class EpmAgentCollectionDetail {
    [string]$CollectionType
    [string]$CollectionUid
    [string]$Value
}

class EpmAgentCollectionSummary {
    [string]$CollectionType
    [int]$Count
}

function Resolve-KeeperEpmAgent {
    <#
    .Synopsis
        Resolve agent(s) by UID or machine name (case-insensitive). Returns matching agent(s) as an array.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string] $Identifier,
        [Parameter(Mandatory = $true)]
        [object] $Plugin
    )
    $uid = $Identifier.Trim()
    if ([string]::IsNullOrEmpty($uid)) { return @() }

    $agent = $Plugin.Agents.GetEntity($uid)
    if ($null -ne $agent) { return @($agent) }

    $lUid = $uid.ToLowerInvariant()
    [array]$matched = @($Plugin.Agents.GetAll() | Where-Object { $null -ne $_ -and $_.MachineId -and $_.MachineId.ToLowerInvariant() -eq $lUid })
    return $matched
}

function script:Resolve-KeeperEpmSingleAgent {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)][string]$Identifier,
        [Parameter(Mandatory = $true)][object]$Plugin
    )
    [array]$agents = @(Resolve-KeeperEpmAgent -Identifier $Identifier -Plugin $Plugin | Where-Object { $null -ne $_ })
    if ($agents.Count -eq 0) {
        Write-Error -Message "Agent '$Identifier' not found." -ErrorAction Stop
    }
    if ($agents.Count -gt 1) {
        Write-Warning "Multiple agents found with machine name `"$Identifier`":"
        foreach ($a in $agents) {
            Write-Warning " UID: $($a.AgentUid) Machine: $($a.MachineId)"
        }
        Write-Error -Message "Machine name `"$Identifier`" is not unique. Use Agent UID." -ErrorAction Stop
    }
    return $agents[0]
}

function Get-KeeperEpmAgentList {
    <#
    .Synopsis
        List all EPM agents.
    .Description
        Takes no parameters; outputs a table of agents with deployment, machine name, and status.
    #>

    [CmdletBinding()]
    Param ()

    $plugin = ensureEpmPlugin
    if (-not $plugin) {
        Write-Error -Message "EPM plugin is not available. Enterprise admin access is required." -ErrorAction Stop
    }

    [array]$agents = @($plugin.Agents.GetAll() | Where-Object { $null -ne $_ } | Sort-Object -Property AgentUid)
    if ($agents.Count -eq 0) {
        Write-Output "No agents found."
        return
    }

    $rows = [System.Collections.Generic.List[EpmAgentInfo]]::new()
    foreach ($ag in $agents) {
        $deploymentName = ''
        if (-not [string]::IsNullOrEmpty($ag.DeploymentUid)) {
            $dep = $plugin.Deployments.GetEntity($ag.DeploymentUid)
            if ($dep) { $deploymentName = if ($dep.Name) { $dep.Name } else { $ag.DeploymentUid } }
            else { $deploymentName = $ag.DeploymentUid }
        }
        $machineName = if ($ag.MachineId) { $ag.MachineId } else { '' }
        $disabled = if ($ag.Disabled) { 'True' } else { 'False' }
        $created = [DateTimeOffset]::FromUnixTimeMilliseconds($ag.Created).ToString("yyyy-MM-dd HH:mm:ss")
        $modified = [DateTimeOffset]::FromUnixTimeMilliseconds($ag.Modified).ToString("yyyy-MM-dd HH:mm:ss")
        $row = [EpmAgentInfo]::new()
        $row.AgentUid    = $ag.AgentUid
        $row.MachineName = $machineName
        $row.Deployment  = $deploymentName
        $row.Disabled    = $disabled
        $row.Created     = $created
        $row.Modified    = $modified
        $rows.Add($row)
    }
    $rows | Format-Table -AutoSize
}

function Get-KeeperEpmAgent {
    <#
    .Synopsis
        View a single EPM agent by UID or machine name.
    .Parameter AgentUidOrName
        Agent UID or machine name (case-insensitive).
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true, Position = 0)]
        [string] $AgentUidOrName
    )

    $plugin = ensureEpmPlugin
    if (-not $plugin) {
        Write-Error -Message "EPM plugin is not available. Enterprise admin access is required." -ErrorAction Stop
    }

    $agent = Resolve-KeeperEpmSingleAgent -Identifier $AgentUidOrName -Plugin $plugin

    $created = [DateTimeOffset]::FromUnixTimeMilliseconds($agent.Created).ToString("yyyy-MM-dd HH:mm:ss")
    $modified = [DateTimeOffset]::FromUnixTimeMilliseconds($agent.Modified).ToString("yyyy-MM-dd HH:mm:ss")
    Write-Output "Agent: $($agent.MachineId)"
    Write-Output " UID: $($agent.AgentUid)"
    Write-Output " Status: $(if ($agent.Disabled) { 'Disabled' } else { 'Active' })"
    if (-not [string]::IsNullOrEmpty($agent.DeploymentUid)) {
        Write-Output " Deployment: $($agent.DeploymentUid)"
    }
    Write-Output " Created: $created"
    Write-Output " Modified: $modified"
}

function Update-KeeperEpmAgent {
    <#
    .Synopsis
        Update EPM agent(s) - deployment and/or enable/disable.
    .Parameter AgentUidOrName
        One or more agent UIDs or machine names.
    .Parameter DeploymentUid
        Deployment UID to assign, if changing deployment.
    .Parameter Enable
        Use 'on' or 'off' to enable or disable the agent(s).
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [string[]] $AgentUidOrName,
        [Parameter()]
        [string] $DeploymentUid,
        [Parameter()]
        [ValidateSet('on', 'off')]
        [string] $Enable
    )

    $plugin = ensureEpmPlugin
    if (-not $plugin) {
        Write-Error -Message "EPM plugin is not available. Enterprise admin access is required." -ErrorAction Stop
    }

    [array]$identifiers = @($AgentUidOrName | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
    if ($identifiers.Count -eq 0) {
        Write-Error -Message "Agent UID(s) or machine name(s) are required for 'update' command." -ErrorAction Stop
    }

    $deploymentUidValue = $null
    if (-not [string]::IsNullOrWhiteSpace($DeploymentUid)) {
        $deployment = $plugin.Deployments.GetEntity($DeploymentUid.Trim())
        if (-not $deployment) {
            Write-Error -Message "Deployment `"$DeploymentUid`" does not exist." -ErrorAction Stop
        }
        $deploymentUidValue = $DeploymentUid.Trim()
    }

    $disabledValue = $null
    if (-not [string]::IsNullOrWhiteSpace($Enable)) {
        $enableLower = $Enable.Trim().ToLowerInvariant()
        if ($enableLower -eq 'on') { $disabledValue = $false }
        elseif ($enableLower -eq 'off') { $disabledValue = $true }
    }

    $updateAgents = [System.Collections.Generic.List[KeeperSecurity.Plugins.EPM.UpdateAgent]]::new()
    foreach ($au in $identifiers) {
        try {
            $agent = Resolve-KeeperEpmSingleAgent -Identifier $au -Plugin $plugin
        } catch {
            Write-Warning "$($_.Exception.Message) Skipping."
            continue
        }
        $ua = New-Object KeeperSecurity.Plugins.EPM.UpdateAgent
        $ua.AgentUid = $agent.AgentUid
        $ua.DeploymentUid = $deploymentUidValue
        $ua.Disabled = $disabledValue
        $updateAgents.Add($ua)
    }

    if ($updateAgents.Count -eq 0) { return }

    try {
        $updateStatus = $plugin.ModifyAgents($updateAgents, $null).GetAwaiter().GetResult()

        $hasErrors = $false
        if ($updateStatus.UpdateErrors -and $updateStatus.UpdateErrors.Count -gt 0) {
            foreach ($err in $updateStatus.UpdateErrors) {
                if (-not $err.Success) {
                    Write-Error -Message "Failed to update agent `"$($err.EntityUid)`": $($err.Message)" -ErrorAction Continue
                    $hasErrors = $true
                }
            }
        }
        if ($updateStatus.Update -and $updateStatus.Update.Count -gt 0) {
            Write-Output "$($updateStatus.Update.Count) agent(s) updated."
        } elseif (-not $hasErrors) {
            Write-Warning "No agents were updated. Check server response."
        }
        writeEpmModifyStatus -Status $updateStatus
        $plugin.SyncDown($false).GetAwaiter().GetResult() | Out-Null
    } catch {
        Write-Error -Message "Error updating agent(s): $($_.Exception.Message)" -ErrorAction Stop
    }
}

function Remove-KeeperEpmAgent {
    <#
    .Synopsis
        Remove an EPM agent by UID or machine name.
    .Parameter AgentUidOrName
        Agent UID or machine name (case-insensitive).
    .Parameter Force
        If set, skip confirmation prompt before delete.
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    Param (
        [Parameter(Mandatory = $true, Position = 0)]
        [string] $AgentUidOrName,
        [Parameter()]
        [switch] $Force
    )

    $plugin = ensureEpmPlugin
    if (-not $plugin) {
        Write-Error -Message "EPM plugin is not available. Enterprise admin access is required." -ErrorAction Stop
    }

    $agent = Resolve-KeeperEpmSingleAgent -Identifier $AgentUidOrName -Plugin $plugin

    $label = if ($agent.MachineId) { $agent.MachineId } else { $agent.AgentUid }
    if (-not $Force -and -not $PSCmdlet.ShouldProcess("agent '$label'", "Delete")) {
        return
    }

    try {
        $removeStatus = $plugin.ModifyAgents($null, [string[]]@($agent.AgentUid)).GetAwaiter().GetResult()

        if ($removeStatus.RemoveErrors -and $removeStatus.RemoveErrors.Count -gt 0) {
            $err = $removeStatus.RemoveErrors[0]
            Write-Error -Message "Failed to remove agent `"$($err.EntityUid)`": $($err.Message)" -ErrorAction Stop
        }
        if ($removeStatus.Remove -and $removeStatus.Remove.Count -gt 0) {
            Write-Output "Agent '$($agent.AgentUid)' removed."
        } else {
            Write-Warning "No agent was removed. Check server response."
        }
        writeEpmModifyStatus -Status $removeStatus
        $plugin.SyncDown($false).GetAwaiter().GetResult() | Out-Null
    } catch {
        Write-Error -Message "Error removing agent: $($_.Exception.Message)" -ErrorAction Stop
    }
}

function Get-KeeperEpmAgentCollection {
    <#
    .Synopsis
        List collections linked to an EPM agent.
    .Description
        By default shows collection type and count. Use -CollectionVerbose for type, UID, and value per collection.
    .Parameter AgentUid
        The agent UID.
    .Parameter CollectionType
        Optional collection type number to filter results.
    .Parameter CollectionVerbose
        If set, show each collection's type, UID, and value instead of grouped counts.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true, Position = 0)]
        [string] $AgentUid,
        [Parameter()]
        [int] $CollectionType = -1,
        [Parameter()]
        [switch] $CollectionVerbose
    )

    $plugin = ensureEpmPlugin
    if (-not $plugin) {
        Write-Error -Message "EPM plugin is not available. Enterprise admin access is required." -ErrorAction Stop
    }

    $agent = Resolve-KeeperEpmSingleAgent -Identifier $AgentUid -Plugin $plugin

    [array]$resourceUids = @($plugin.CollectionLinks.GetLinksForObject($agent.AgentUid) | Where-Object { $null -ne $_ -and $_.LinkType -eq [int][PEDM.CollectionLinkType]::CltAgent } | ForEach-Object { $_.CollectionUid })

    $collections = [System.Collections.Generic.List[object]]::new()
    foreach ($uid in $resourceUids) {
        $c = $plugin.Collections.GetEntity($uid)
        if ($null -ne $c) { $collections.Add($c) }
    }

    if ($PSBoundParameters.ContainsKey('CollectionType')) {
        [array]$filtered = @($collections | Where-Object { $_.CollectionType -eq $CollectionType })
        $collections = $filtered
    }

    if ($collections.Count -eq 0) {
        Write-Output "No collections found for agent '$($agent.AgentUid)'."
        return
    }

    if ($CollectionVerbose) {
        $collections = $collections | Sort-Object -Property CollectionType, CollectionUid
        $rows = [System.Collections.Generic.List[EpmAgentCollectionDetail]]::new()
        foreach ($collection in $collections) {
            $typeName = getEpmCollectionTypeName -CollectionType $collection.CollectionType
            $value = ''
            if ($collection.CollectionData -and $collection.CollectionData.Length -gt 0) {
                try {
                    $jsonStr = [System.Text.Encoding]::UTF8.GetString($collection.CollectionData)
                    $data = $jsonStr | ConvertFrom-Json
                    $parts = [System.Collections.Generic.List[string]]::new()
                    $data.PSObject.Properties | ForEach-Object { $parts.Add("$($_.Name)=$($_.Value)") }
                    $value = $parts -join ', '
                } catch {
                    $value = "(binary data, $($collection.CollectionData.Length) bytes)"
                }
            }
            $row = [EpmAgentCollectionDetail]::new()
            $row.CollectionType = "$typeName ($($collection.CollectionType))"
            $row.CollectionUid  = $collection.CollectionUid
            $row.Value          = $value
            $rows.Add($row)
        }
        $rows | Format-Table -AutoSize
    } else {
        $grouped = $collections | Group-Object -Property CollectionType | Sort-Object -Property Name
        $rows = [System.Collections.Generic.List[EpmAgentCollectionSummary]]::new()
        foreach ($g in $grouped) {
            $typeName = getEpmCollectionTypeName -CollectionType $g.Name
            $row = [EpmAgentCollectionSummary]::new()
            $row.CollectionType = "$typeName ($($g.Name))"
            $row.Count          = $g.Count
            $rows.Add($row)
        }
        $rows | Format-Table -AutoSize
    }
}

New-Alias -Name kepm-agent-list        -Value Get-KeeperEpmAgentList        -ErrorAction SilentlyContinue
New-Alias -Name kepm-agent-view        -Value Get-KeeperEpmAgent            -ErrorAction SilentlyContinue
New-Alias -Name kepm-agent-edit        -Value Update-KeeperEpmAgent          -ErrorAction SilentlyContinue
New-Alias -Name kepm-agent-delete      -Value Remove-KeeperEpmAgent         -ErrorAction SilentlyContinue
New-Alias -Name kepm-agent-collection  -Value Get-KeeperEpmAgentCollection   -ErrorAction SilentlyContinue

# SIG # Begin signature block
# MIInvgYJKoZIhvcNAQcCoIInrzCCJ6sCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBj3UBlZDq4mIQn
# qwcJRszFWYEbVkgSFMDkakxFt+q+x6CCITswggWNMIIEdaADAgECAhAOmxiO+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
# twGpn1eqXijiuZQwggawMIIEmKADAgECAhAIrUCyYNKcTJ9ezam9k67ZMA0GCSqG
# SIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx
# GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy
# dXN0ZWQgUm9vdCBHNDAeFw0yMTA0MjkwMDAwMDBaFw0zNjA0MjgyMzU5NTlaMGkx
# CzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4
# RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEzODQg
# MjAyMSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDVtC9C0Cit
# eLdd1TlZG7GIQvUzjOs9gZdwxbvEhSYwn6SOaNhc9es0JAfhS0/TeEP0F9ce2vnS
# 1WcaUk8OoVf8iJnBkcyBAz5NcCRks43iCH00fUyAVxJrQ5qZ8sU7H/Lvy0daE6ZM
# swEgJfMQ04uy+wjwiuCdCcBlp/qYgEk1hz1RGeiQIXhFLqGfLOEYwhrMxe6TSXBC
# Mo/7xuoc82VokaJNTIIRSFJo3hC9FFdd6BgTZcV/sk+FLEikVoQ11vkunKoAFdE3
# /hoGlMJ8yOobMubKwvSnowMOdKWvObarYBLj6Na59zHh3K3kGKDYwSNHR7OhD26j
# q22YBoMbt2pnLdK9RBqSEIGPsDsJ18ebMlrC/2pgVItJwZPt4bRc4G/rJvmM1bL5
# OBDm6s6R9b7T+2+TYTRcvJNFKIM2KmYoX7BzzosmJQayg9Rc9hUZTO1i4F4z8ujo
# 7AqnsAMrkbI2eb73rQgedaZlzLvjSFDzd5Ea/ttQokbIYViY9XwCFjyDKK05huzU
# tw1T0PhH5nUwjewwk3YUpltLXXRhTT8SkXbev1jLchApQfDVxW0mdmgRQRNYmtwm
# KwH0iU1Z23jPgUo+QEdfyYFQc4UQIyFZYIpkVMHMIRroOBl8ZhzNeDhFMJlP/2NP
# TLuqDQhTQXxYPUez+rbsjDIJAsxsPAxWEQIDAQABo4IBWTCCAVUwEgYDVR0TAQH/
# BAgwBgEB/wIBADAdBgNVHQ4EFgQUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHwYDVR0j
# BBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1Ud
# JQQMMAoGCCsGAQUFBwMDMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0
# cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0
# cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8E
# PDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVz
# dGVkUm9vdEc0LmNybDAcBgNVHSAEFTATMAcGBWeBDAEDMAgGBmeBDAEEATANBgkq
# hkiG9w0BAQwFAAOCAgEAOiNEPY0Idu6PvDqZ01bgAhql+Eg08yy25nRm95RysQDK
# r2wwJxMSnpBEn0v9nqN8JtU3vDpdSG2V1T9J9Ce7FoFFUP2cvbaF4HZ+N3HLIvda
# qpDP9ZNq4+sg0dVQeYiaiorBtr2hSBh+3NiAGhEZGM1hmYFW9snjdufE5BtfQ/g+
# lP92OT2e1JnPSt0o618moZVYSNUa/tcnP/2Q0XaG3RywYFzzDaju4ImhvTnhOE7a
# brs2nfvlIVNaw8rpavGiPttDuDPITzgUkpn13c5UbdldAhQfQDN8A+KVssIhdXNS
# y0bYxDQcoqVLjc1vdjcshT8azibpGL6QB7BDf5WIIIJw8MzK7/0pNVwfiThV9zeK
# iwmhywvpMRr/LhlcOXHhvpynCgbWJme3kuZOX956rEnPLqR0kq3bPKSchh/jwVYb
# KyP/j7XqiHtwa+aguv06P0WmxOgWkVKLQcBIhEuWTatEQOON8BUozu3xGFYHKi8Q
# xAwIZDwzj64ojDzLj4gLDb879M4ee47vtevLt/B3E+bnKD+sEq6lLyJsQfmCXBVm
# zGwOysWGw/YmMwwHS6DTBwJqakAwSEs0qFEgu60bhQjiWQ1tygVQK+pKHJ6l/aCn
# HwZ05/LWUpD9r4VIIflXO7ScA+2GRfS0YW6/aOImYIbqyK+p/pQd52MbOoZWeE4w
# gga0MIIEnKADAgECAhANx6xXBf8hmS5AQyIMOkmGMA0GCSqGSIb3DQEBCwUAMGIx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH
# NDAeFw0yNTA1MDcwMDAwMDBaFw0zODAxMTQyMzU5NTlaMGkxCzAJBgNVBAYTAlVT
# MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1
# c3RlZCBHNCBUaW1lU3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYgMjAyNSBDQTEwggIi
# MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC0eDHTCphBcr48RsAcrHXbo0Zo
# dLRRF51NrY0NlLWZloMsVO1DahGPNRcybEKq+RuwOnPhof6pvF4uGjwjqNjfEvUi
# 6wuim5bap+0lgloM2zX4kftn5B1IpYzTqpyFQ/4Bt0mAxAHeHYNnQxqXmRinvuNg
# xVBdJkf77S2uPoCj7GH8BLuxBG5AvftBdsOECS1UkxBvMgEdgkFiDNYiOTx4OtiF
# cMSkqTtF2hfQz3zQSku2Ws3IfDReb6e3mmdglTcaarps0wjUjsZvkgFkriK9tUKJ
# m/s80FiocSk1VYLZlDwFt+cVFBURJg6zMUjZa/zbCclF83bRVFLeGkuAhHiGPMvS
# GmhgaTzVyhYn4p0+8y9oHRaQT/aofEnS5xLrfxnGpTXiUOeSLsJygoLPp66bkDX1
# ZlAeSpQl92QOMeRxykvq6gbylsXQskBBBnGy3tW/AMOMCZIVNSaz7BX8VtYGqLt9
# MmeOreGPRdtBx3yGOP+rx3rKWDEJlIqLXvJWnY0v5ydPpOjL6s36czwzsucuoKs7
# Yk/ehb//Wx+5kMqIMRvUBDx6z1ev+7psNOdgJMoiwOrUG2ZdSoQbU2rMkpLiQ6bG
# RinZbI4OLu9BMIFm1UUl9VnePs6BaaeEWvjJSjNm2qA+sdFUeEY0qVjPKOWug/G6
# X5uAiynM7Bu2ayBjUwIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAd
# BgNVHQ4EFgQU729TSunkBnx6yuKQVvYv1Ensy04wHwYDVR0jBBgwFoAU7NfjgtJx
# XWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUF
# BwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGln
# aWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5j
# b20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJo
# dHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNy
# bDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQEL
# BQADggIBABfO+xaAHP4HPRF2cTC9vgvItTSmf83Qh8WIGjB/T8ObXAZz8OjuhUxj
# aaFdleMM0lBryPTQM2qEJPe36zwbSI/mS83afsl3YTj+IQhQE7jU/kXjjytJgnn0
# hvrV6hqWGd3rLAUt6vJy9lMDPjTLxLgXf9r5nWMQwr8Myb9rEVKChHyfpzee5kH0
# F8HABBgr0UdqirZ7bowe9Vj2AIMD8liyrukZ2iA/wdG2th9y1IsA0QF8dTXqvcnT
# mpfeQh35k5zOCPmSNq1UH410ANVko43+Cdmu4y81hjajV/gxdEkMx1NKU4uHQcKf
# ZxAvBAKqMVuqte69M9J6A47OvgRaPs+2ykgcGV00TYr2Lr3ty9qIijanrUR3anzE
# wlvzZiiyfTPjLbnFRsjsYg39OlV8cipDoq7+qNNjqFzeGxcytL5TTLL4ZaoBdqbh
# OhZ3ZRDUphPvSRmMThi0vw9vODRzW6AxnJll38F0cuJG7uEBYTptMSbhdhGQDpOX
# gpIUsWTjd6xpR6oaQf/DJbg3s6KCLPAlZ66RzIg9sC+NJpud/v4+7RWsWCiKi9EO
# LLHfMR2ZyJ/+xhCx9yHbxtl5TPau1j/1MIDpMPx0LckTetiSuEtQvLsNz3Qbp7wG
# WqbIiOWCnb5WqxL3/BAPvIXKUjPSxyZsq8WhbaM2tszWkPZPubdcMIIG7TCCBNWg
# AwIBAgIQCoDvGEuN8QWC0cR2p5V0aDANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQG
# EwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0
# IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUgQ0Ex
# MB4XDTI1MDYwNDAwMDAwMFoXDTM2MDkwMzIzNTk1OVowYzELMAkGA1UEBhMCVVMx
# FzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBTSEEy
# NTYgUlNBNDA5NiBUaW1lc3RhbXAgUmVzcG9uZGVyIDIwMjUgMTCCAiIwDQYJKoZI
# hvcNAQEBBQADggIPADCCAgoCggIBANBGrC0Sxp7Q6q5gVrMrV7pvUf+GcAoB38o3
# zBlCMGMyqJnfFNZx+wvA69HFTBdwbHwBSOeLpvPnZ8ZN+vo8dE2/pPvOx/Vj8Tch
# TySA2R4QKpVD7dvNZh6wW2R6kSu9RJt/4QhguSssp3qome7MrxVyfQO9sMx6ZAWj
# FDYOzDi8SOhPUWlLnh00Cll8pjrUcCV3K3E0zz09ldQ//nBZZREr4h/GI6Dxb2Uo
# yrN0ijtUDVHRXdmncOOMA3CoB/iUSROUINDT98oksouTMYFOnHoRh6+86Ltc5zjP
# KHW5KqCvpSduSwhwUmotuQhcg9tw2YD3w6ySSSu+3qU8DD+nigNJFmt6LAHvH3KS
# uNLoZLc1Hf2JNMVL4Q1OpbybpMe46YceNA0LfNsnqcnpJeItK/DhKbPxTTuGoX7w
# JNdoRORVbPR1VVnDuSeHVZlc4seAO+6d2sC26/PQPdP51ho1zBp+xUIZkpSFA8vW
# doUoHLWnqWU3dCCyFG1roSrgHjSHlq8xymLnjCbSLZ49kPmk8iyyizNDIXj//cOg
# rY7rlRyTlaCCfw7aSUROwnu7zER6EaJ+AliL7ojTdS5PWPsWeupWs7NpChUk555K
# 096V1hE0yZIXe+giAwW00aHzrDchIc2bQhpp0IoKRR7YufAkprxMiXAJQ1XCmnCf
# gPf8+3mnAgMBAAGjggGVMIIBkTAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTkO/zy
# Me39/dfzkXFjGVBDz2GM6DAfBgNVHSMEGDAWgBTvb1NK6eQGfHrK4pBW9i/USezL
# TjAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwgZUGCCsG
# AQUFBwEBBIGIMIGFMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5j
# b20wXQYIKwYBBQUHMAKGUWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdp
# Q2VydFRydXN0ZWRHNFRpbWVTdGFtcGluZ1JTQTQwOTZTSEEyNTYyMDI1Q0ExLmNy
# dDBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGln
# aUNlcnRUcnVzdGVkRzRUaW1lU3RhbXBpbmdSU0E0MDk2U0hBMjU2MjAyNUNBMS5j
# cmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9bAcBMA0GCSqGSIb3DQEB
# CwUAA4ICAQBlKq3xHCcEua5gQezRCESeY0ByIfjk9iJP2zWLpQq1b4URGnwWBdEZ
# D9gBq9fNaNmFj6Eh8/YmRDfxT7C0k8FUFqNh+tshgb4O6Lgjg8K8elC4+oWCqnU/
# ML9lFfim8/9yJmZSe2F8AQ/UdKFOtj7YMTmqPO9mzskgiC3QYIUP2S3HQvHG1FDu
# +WUqW4daIqToXFE/JQ/EABgfZXLWU0ziTN6R3ygQBHMUBaB5bdrPbF6MRYs03h4o
# bEMnxYOX8VBRKe1uNnzQVTeLni2nHkX/QqvXnNb+YkDFkxUGtMTaiLR9wjxUxu2h
# ECZpqyU1d0IbX6Wq8/gVutDojBIFeRlqAcuEVT0cKsb+zJNEsuEB7O7/cuvTQasn
# M9AWcIQfVjnzrvwiCZ85EE8LUkqRhoS3Y50OHgaY7T/lwd6UArb+BOVAkg2oOvol
# /DJgddJ35XTxfUlQ+8Hggt8l2Yv7roancJIFcbojBcxlRcGG0LIhp6GvReQGgMgY
# xQbV1S3CrWqZzBt1R9xJgKf47CdxVRd/ndUlQ05oxYy2zRWVFjF7mcr4C34Mj3oc
# CVccAvlKV9jEnstrniLvUxxVZE/rptb7IRE2lskKPIJgbaP5t2nGj/ULLi49xTcB
# ZU8atufk+EMF/cWuiC7POGT75qaL6vdCvHlshtjdNXOCIUjsarfNZzCCB0kwggUx
# oAMCAQICEAe0P3SLJmcoVNrErUyxTt0wDQYJKoZIhvcNAQELBQAwaTELMAkGA1UE
# BhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2Vy
# dCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNIQTM4NCAyMDIxIENB
# MTAeFw0yNTEyMzEwMDAwMDBaFw0yOTAxMDIyMzU5NTlaMIHRMRMwEQYLKwYBBAGC
# NzwCAQMTAlVTMRkwFwYLKwYBBAGCNzwCAQITCERlbGF3YXJlMR0wGwYDVQQPDBRQ
# cml2YXRlIE9yZ2FuaXphdGlvbjEQMA4GA1UEBRMHMzQwNzk4NTELMAkGA1UEBhMC
# VVMxETAPBgNVBAgTCElsbGlub2lzMRAwDgYDVQQHEwdDaGljYWdvMR0wGwYDVQQK
# ExRLZWVwZXIgU2VjdXJpdHkgSW5jLjEdMBsGA1UEAxMUS2VlcGVyIFNlY3VyaXR5
# IEluYy4wggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCUcNMoSVmxAi0a
# vG+StFJMNFFTUIOo3HdBZ+0gqA1XpNgUx11vB1vCZrvFsD9m5oA58tdp4gZN3LmQ
# aMvCl2ANUT7MilI02Hf1RWlygBzon6iE0GpU3lgRrwrk1dhtLpGsR6dbMKUUHprc
# vKpXk90/VN+vhzY1uik1tCTxkDCPu/AYJg7m9+tR2KqvMuYMaMLhii66eWUAGsBC
# h/uZxjkGoJF6qZ0DgFd7rW7VYljbfYSNPeZNGTDgB0J/wOsKl0mn612DTseIvAKt
# 4vra/FLFukyEyStnfQ8lWYDcLLCMCjNVrzGipmT5E2iyx7Y1RZCIpNwVogp3Ixbk
# Gbq5A/41YNOLLd4cFewyB2F037RevBCRsUODZEt1qBf7Jbu3DiYo1G+zTj9E0R1s
# FzyijcfdsTm6X5ble+yCJeGkX5XgsyPnZpyz/FX9Fr0N9pMPGWwW2PKyHEnSytXm
# 0Dxdq2P4mA4CBUxq7YoV26L2PF6QEh9BQdXTPcnLysUv7SI/a0ECAwEAAaOCAgIw
# ggH+MB8GA1UdIwQYMBaAFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB0GA1UdDgQWBBRG
# 4H6CH8pvNX632bsdnrda4MtJLDA9BgNVHSAENjA0MDIGBWeBDAEDMCkwJwYIKwYB
# BQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAOBgNVHQ8BAf8EBAMC
# B4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwgbUGA1UdHwSBrTCBqjBToFGgT4ZNaHR0
# cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25p
# bmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcmwwU6BRoE+GTWh0dHA6Ly9jcmw0LmRp
# Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNI
# QTM4NDIwMjFDQTEuY3JsMIGUBggrBgEFBQcBAQSBhzCBhDAkBggrBgEFBQcwAYYY
# aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBodHRwOi8vY2Fj
# ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JT
# QTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUA
# A4ICAQA1Wlq0WzJa3N6DgjgBU7nagIJBab1prPARXZreX1MOv9VjnS5o0CrfQLr6
# z3bmWHw7xT8dt6bcSwRixqvPJtv4q8Rvo80O3eUMvMxQzqmi7z1zf+HG+/3G4F+2
# IYegvPc8Ui151XCV9rjA8tvFWRLRMX0ZRxY1zfT027HMw0iYL20z44+Cky//FAnL
# iRwoNDGiRkZiHbB9YOftPAYNMG3gm1z3zOW5RdfKPrqvMuijE+dfyLIAA6Immpzu
# FMH+Wgn8NnSlot9b4YKycaqqdjd7wXDjPub/oQ7VShuCSBWj+UNOTVh0vcZGackc
# H1DLVgwp2dcKlxJiQKtkHT/T6LloY6LTe6+8wkVkr8EAv1W+q/+M1a4Ao+ykFbIA
# 2LBEmA9qdgoLtenAYIiEg+48SjMPgyBbVPE3bhL1vIqjEIxYCfdmi6wx33oYX7HB
# +bJ7zitHw4GgtpfPV8y8QRZImKmeDOKyXjQPDmQM/Eglm/Ns0GzBkVXM8h6UI34b
# WZrHz9sbLSE20m5Svmxftvw5zju+I3WsmS/stNfWlOkwU0niUgwPHaz21kjXEA5A
# g+aqv26wodqZcnGOlChoWDvSJ8KKgdOFbeAYKAMp1NY7iWV315zpGH19RipCR1NH
# 0ND8iIubk3WGNf2rzEfqlOi3h2ywqVkU6AKXHdO5JV4otSKKEDGCBdkwggXVAgEB
# MH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYD
# VQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNI
# QTM4NCAyMDIxIENBMQIQB7Q/dIsmZyhU2sStTLFO3TANBglghkgBZQMEAgEFAKCB
# hDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEE
# AYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJ
# BDEiBCCs5HODLYqlRWFIlKVK38fv36aIP9qXa710TSiofnJxwTANBgkqhkiG9w0B
# AQEFAASCAYBzR0Jfa69A7q9y2MxvDNejrHhcW2TgZzk/eFkIZKre9C2n6ifaGVBe
# SWArPDZ/IYUd5aFPKgEyh5mwpXBMuxCX81/Z+Y4YyiiAw0Si7f+4DBYR1abmOndd
# ZGz4mo54gw3qzPR/7RkXMMRXWCDPJzpyW+bhfbGAkV/C+VI2mu7nAaTIFLyz1R95
# 9E1cZje5y+ZsmZYpF9f/1N0SpXPCvSbZ6AtgvaqM3w0MenNbUpfviVhhCQtsrXeL
# i+Gb0gceIluTfThlkKBka31rsGnd6B022GWZ4x9JcLp4rPSzE+NUyRKGD9Wz9bRf
# FzM/M5NyDo3giQdqSPJWJH8mPPWvplB9H19s+qU0gDNiqch43jONDG0nLLDrxhmH
# SRXZgrx7DMkT4xqx4xdRtnIUxG/JMt69TxwUO0Ayt4G8DXYSeQW0KTOP/pr1PCk4
# 5MTNtykzJHEn6wWI3t8B1D7rDiDHEAwG8MrnG1DbXs+8431mSeyW6qC0BlszzGMI
# qYhCZjSq/UChggMmMIIDIgYJKoZIhvcNAQkGMYIDEzCCAw8CAQEwfTBpMQswCQYD
# VQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lD
# ZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUg
# Q0ExAhAKgO8YS43xBYLRxHanlXRoMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcN
# AQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjYwNTI2MDQwNjQ2WjAv
# BgkqhkiG9w0BCQQxIgQgC2WDNkGUyFtjSxm/PeREZJn8MknNBmH5ANB2W37dTLYw
# DQYJKoZIhvcNAQEBBQAEggIArRmVHMnh8XlR5vFbot8ho1V8GEmQh/GWUHwheTUm
# YeOEHdf6lR7LRzxNBt8oRuuBo4510YLU739ZAReWqk4lKeUne8Il6Ft/MNc8uXLZ
# DZ9U9XfMj/8W8Vsf7P2jYkkRv/x5/C9cDhrrnCNdTPFGGWfRN6Y3eAf98Em8np68
# df/d+FsS+Z2J/vvK71Mlx5mVpMHuiioWe0x9ne/OiVLD1PY1n6qysA2Sj4NQoSxK
# s6OE14dQohOwhNYl119j14uu1liQlfP7LRVrpIvCkwbaeMaXmjASshcXIDvph2tJ
# zve84QEozX+SM4uab7lBtJSCk7eBU26Tz4AYbKsEaB4+rjROjc+8Jdevgzt76UWW
# Ahgs1csmAON3c2f77Zwi4rpWp9dn8AxBQcOzOU8Jk0w9wWbphVi8OOZNi1xNk2f0
# C0IyFKeXbS/4Y7AaseDaUuf8ALUn0Z/OxaISWtNNsW1oXPz7BpsqK+YN5wd0Jy2r
# vCApJpUlEYmnA/iqtKRJjIPPrdlB5GOhr4+rlmbjfy74PGS0O9QGV0F9hDSQ84K0
# qdvFrYAcmHUXiOkWz/5gRt2kuycEtpKNKUz2Fc/TrrU4nDDkA0ldVwMwrhR0spon
# Rz6ke/T8AR6JcIOgCLFsF/xHjxol/7nyi4vKtgAvQMBfJrybPZoW1xH3kMr8WYNB
# otI=
# SIG # End signature block