EPM/Deployment.ps1

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

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

    $deployment = $Plugin.Deployments.GetEntity($id)
    if ($null -ne $deployment) { return @($deployment) }

    $lName = $id.ToLowerInvariant()
    return @($Plugin.Deployments.GetAll() | Where-Object { $_.Name -and $_.Name.ToLowerInvariant() -eq $lName })
}

function script:Resolve-KeeperEpmSingleDeployment {
    param(
        [Parameter(Mandatory = $true)][string]$Identifier,
        [Parameter(Mandatory = $true)][object]$Plugin
    )
    $deployments = @(Resolve-KeeperEpmDeployment -Identifier $Identifier -Plugin $Plugin)
    if ($deployments.Count -eq 0) {
        Write-Error -Message "Deployment '$Identifier' not found." -ErrorAction Stop
    }
    if ($deployments.Count -gt 1) {
        Write-Warning "Multiple deployments found with name `"$Identifier`":"
        foreach ($d in $deployments) {
            Write-Warning " UID: $($d.DeploymentUid) Name: $($d.Name)"
        }
        Write-Error -Message "Deployment name `"$Identifier`" is not unique. Use Deployment UID." -ErrorAction Stop
    }
    return $deployments[0]
}

function Get-KeeperEpmDeploymentList {
    <#
    .Synopsis
        List all EPM deployments.
    .Description
        Takes no parameters. Lists deployment UID, name, disabled state, created/modified timestamps, and agent count.
    #>

    [CmdletBinding()]
    Param ()

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

    $deployments = @($plugin.Deployments.GetAll())
    if ($deployments.Count -eq 0) {
        Write-Output "No deployments found."
        return
    }

    $deployments = $deployments | Sort-Object -Property Name
    $rows = foreach ($dep in $deployments) {
        $agentCount = @($plugin.DeploymentAgents.GetLinksForSubject($dep.DeploymentUid)).Count
        $created = [DateTimeOffset]::FromUnixTimeMilliseconds($dep.Created).ToString("yyyy-MM-dd HH:mm:ss")
        $updated = [DateTimeOffset]::FromUnixTimeMilliseconds($dep.Modified).ToString("yyyy-MM-dd HH:mm:ss")
        [PSCustomObject]@{
            'Deployment UID' = $dep.DeploymentUid
            'Name'           = $dep.Name
            'Disabled'       = if ($dep.Disabled) { 'True' } else { 'False' }
            'Created'        = $created
            'Modified'       = $updated
            'Agent Count'    = $agentCount
        }
    }
    $rows | Format-Table -AutoSize
}

function Get-KeeperEpmDeployment {
    <#
    .Synopsis
        View a single EPM deployment by UID or name.
    .Parameter DeploymentUidOrName
        Deployment UID or deployment name (case-insensitive).
    #>

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

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

    $deployment = Resolve-KeeperEpmSingleDeployment -Identifier $DeploymentUidOrName -Plugin $plugin

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

function Add-KeeperEpmDeployment {
    <#
    .Synopsis
        Add a new EPM deployment.
    .Parameter Name
        Deployment display name.
    .Parameter Force
        If set, allow adding a deployment whose name already exists.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string] $Name,
        [Parameter()]
        [switch] $Force
    )

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

    $nameValue = $Name.Trim()
    if ([string]::IsNullOrEmpty($nameValue)) {
        Write-Error -Message "Deployment name is required for 'add' command." -ErrorAction Stop
    }

    if (-not $Force) {
        $lName = $nameValue.ToLowerInvariant()
        $hasName = $plugin.Deployments.GetAll() | Where-Object { $_.Name -and $_.Name.ToLowerInvariant() -eq $lName }
        if ($hasName) {
            Write-Error -Message "Deployment `"$nameValue`" already exists." -ErrorAction Stop
        }
    }

    $addDeployment = New-Object KeeperSecurity.Plugins.EPM.DeploymentDataInput
    $addDeployment.Name = $nameValue

    $addStatus = $plugin.ModifyDeployments(
        [KeeperSecurity.Plugins.EPM.DeploymentDataInput[]]@($addDeployment),
        $null,
        $null
    ).GetAwaiter().GetResult()

    if ($addStatus.AddErrors -and $addStatus.AddErrors.Count -gt 0) {
        $err = $addStatus.AddErrors[0]
        Write-Error -Message "Failed to add deployment `"$($err.EntityUid)`": $($err.Message)" -ErrorAction Stop
    }
    if ($addStatus.Add -and $addStatus.Add.Count -gt 0) {
        Write-Output "Deployment '$nameValue' added."
    } else {
        Write-Warning "No deployment was added. Check server response."
    }
    writeEpmModifyStatus -Status $addStatus
    $plugin.SyncDown($false).GetAwaiter().GetResult() | Out-Null
}

function Update-KeeperEpmDeployment {
    <#
    .Synopsis
        Update an existing EPM deployment.
    .Parameter DeploymentUidOrName
        Deployment UID or deployment name (case-insensitive).
    .Parameter Name
        New deployment display name.
    .Parameter Enable
        Use 'on' or 'off' to enable or disable the deployment.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true, Position = 0)]
        [string] $DeploymentUidOrName,
        [Parameter()]
        [string] $Name,
        [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
    }

    $deployment = Resolve-KeeperEpmSingleDeployment -Identifier $DeploymentUidOrName -Plugin $plugin

    $updateDeployment = New-Object KeeperSecurity.Plugins.EPM.DeploymentDataInput
    $updateDeployment.DeploymentUid = $deployment.DeploymentUid
    $updateDeployment.Name = if ($Name) { $Name.Trim() } else { $deployment.Name }
    if (-not [string]::IsNullOrWhiteSpace($Enable)) {
        $enableLower = $Enable.Trim().ToLowerInvariant()
        if ($enableLower -eq 'on') { $updateDeployment.Disabled = $false }
        elseif ($enableLower -eq 'off') { $updateDeployment.Disabled = $true }
    }

    $updateStatus = $plugin.ModifyDeployments(
        $null,
        [KeeperSecurity.Plugins.EPM.DeploymentDataInput[]]@($updateDeployment),
        $null
    ).GetAwaiter().GetResult()

    if ($updateStatus.UpdateErrors -and $updateStatus.UpdateErrors.Count -gt 0) {
        $err = $updateStatus.UpdateErrors[0]
        Write-Error -Message "Failed to update deployment `"$($err.EntityUid)`": $($err.Message)" -ErrorAction Stop
    }
    if ($updateStatus.Update -and $updateStatus.Update.Count -gt 0) {
        Write-Output "Deployment '$($deployment.DeploymentUid)' updated."
    } else {
        Write-Warning "No deployment was updated. Check server response."
    }

    writeEpmModifyStatus -Status $updateStatus
    $plugin.SyncDown($false).GetAwaiter().GetResult() | Out-Null
}

function Remove-KeeperEpmDeployment {
    <#
    .Synopsis
        Remove an EPM deployment.
    .Parameter DeploymentUidOrName
        Deployment UID or deployment name (case-insensitive).
    .Parameter Force
        If set, skip confirmation prompt before delete.
    #>

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

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

    $deployment = Resolve-KeeperEpmSingleDeployment -Identifier $DeploymentUidOrName -Plugin $plugin

    $deploymentUid = $deployment.DeploymentUid
    if (-not $Force -and -not $PSCmdlet.ShouldProcess("deployment '$($deployment.Name)'", "Delete")) {
        return
    }

    $removeStatus = $plugin.ModifyDeployments(
        $null,
        $null,
        [string[]]@($deploymentUid)
    ).GetAwaiter().GetResult()

    if ($removeStatus.RemoveErrors -and $removeStatus.RemoveErrors.Count -gt 0) {
        $err = $removeStatus.RemoveErrors[0]
        Write-Error -Message "Failed to delete deployment `"$($err.EntityUid)`": $($err.Message)" -ErrorAction Stop
    }
    if ($removeStatus.Remove -and $removeStatus.Remove.Count -gt 0) {
        Write-Output "Deployment '$deploymentUid' deleted."
    } else {
        Write-Warning "No deployment was deleted. Check server response."
    }

    writeEpmModifyStatus -Status $removeStatus
    $plugin.SyncDown($false).GetAwaiter().GetResult() | Out-Null
}

function Get-KeeperEpmDeploymentDownload {
    <#
    .Synopsis
        Get deployment token and agent download URLs.
    .Description
        Outputs deployment token and Windows/MacOS/Linux download URLs. Optionally writes to a file.
    .Parameter DeploymentUidOrName
        Deployment UID or deployment name (case-insensitive).
    .Parameter File
        Optional path to write token and download lines (tab-separated) as UTF-8.
    #>

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

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

    $deployment = Resolve-KeeperEpmSingleDeployment -Identifier $DeploymentUidOrName -Plugin $plugin

    if (-not $deployment.PrivateKey -or $deployment.PrivateKey.Length -eq 0) {
        Write-Error -Message "Deployment '$($deployment.DeploymentUid)' does not have a private key." -ErrorAction Stop
    }

    $ent = getEnterprise
    $hostName = if ($ent -and $ent.loader -and $ent.loader.Auth -and $ent.loader.Auth.Endpoint -and $ent.loader.Auth.Endpoint.Server) {
        $ent.loader.Auth.Endpoint.Server
    } else {
        'keepersecurity.com'
    }

    Write-Warning "The deployment token contains a private key. Treat it as a secret and do not share it insecurely."

    $privateKeyB64 = [KeeperSecurity.Utils.CryptoUtils]::Base64UrlEncode($deployment.PrivateKey)
    $token = "${hostName}:$($deployment.DeploymentUid):$privateKeyB64"

    $path = ''
    $windows = ''
    $macos = ''
    $linux = ''
    $manifestHost = $hostName
    if ($manifestHost.Contains('.')) {
        $parts = $manifestHost.Split('.')
        if ($parts.Length -ge 2) {
            $manifestHost = $parts[$parts.Length - 2] + '.' + $parts[$parts.Length - 1]
        }
    }

    $manifestUrl = "https://${manifestHost}/pam/pedm/package-manifest.json"
    try {
        $response = Invoke-WebRequest -Uri $manifestUrl -UseBasicParsing -TimeoutSec 10 -ErrorAction Stop
        $manifest = $response.Content | ConvertFrom-Json
        if ($manifest.Core -and $manifest.Core.Count -gt 0) {
            $selected = $manifest.Core | Where-Object { $_.Version -eq 'Latest' } | Select-Object -First 1
            if (-not $selected) { $selected = $manifest.Core[0] }
            $path = $selected.Path
            if (-not [string]::IsNullOrEmpty($path) -and -not $path.EndsWith('/')) { $path += '/' }
            $windows = $selected.WindowsZip
            $macos = $selected.MacOsZip
            $linux = $selected.LinuxZip
        }
    } catch {
        Write-Warning "Failed to fetch manifest from $manifestUrl"
    }

    $fileLines = [System.Collections.Generic.List[string]]::new()
    if (-not [string]::IsNullOrEmpty($path)) {
        $platforms = @(
            @{ Label = 'Windows'; File = $windows }
            @{ Label = 'MacOS';   File = $macos }
            @{ Label = 'Linux';   File = $linux }
        )
        $hasAny = $false
        foreach ($p in $platforms) {
            if (-not [string]::IsNullOrEmpty($p.File)) {
                $url = $path + $p.File
                Write-Output "$($p.Label) download URL`t$url"
                $fileLines.Add("$($p.Label) download URL`t$url")
                $hasAny = $true
            }
        }
        if ($hasAny) {
            $fileLines.Add('')
        }
    }
    Write-Output "Deployment Token`t$token"
    $fileLines.Add("Deployment Token`t$token")

    if (-not [string]::IsNullOrWhiteSpace($File)) {
        $fileLines -join [Environment]::NewLine | Set-Content -Path $File -Encoding UTF8
        Write-Output "Deployment token and download URLs written to: $File"
    }
}

New-Alias -Name kepm-deployment-list    -Value Get-KeeperEpmDeploymentList     -ErrorAction SilentlyContinue
New-Alias -Name kepm-deployment-view    -Value Get-KeeperEpmDeployment         -ErrorAction SilentlyContinue
New-Alias -Name kepm-deployment-add     -Value Add-KeeperEpmDeployment         -ErrorAction SilentlyContinue
New-Alias -Name kepm-deployment-edit    -Value Update-KeeperEpmDeployment      -ErrorAction SilentlyContinue
New-Alias -Name kepm-deployment-delete  -Value Remove-KeeperEpmDeployment      -ErrorAction SilentlyContinue
New-Alias -Name kepm-deployment-download -Value Get-KeeperEpmDeploymentDownload -ErrorAction SilentlyContinue

# SIG # Begin signature block
# MIInvgYJKoZIhvcNAQcCoIInrzCCJ6sCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDAgnXSJ4mm6eoA
# enDri7667A7cS2d10YAWsTne2WCyy6CCITswggWNMIIEdaADAgECAhAOmxiO+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
# BDEiBCCwsbKSBbwfr6fGoSkYfa0iMguiQt1Gtk9aMJgnR7AICTANBgkqhkiG9w0B
# AQEFAASCAYA+YJyZhUek6hcljj7J9DX6GkzzOb5PcHRV2I26PVvFTQz8PssZxhUE
# pIY65sQ/6/agQ1EsHOyH+At1fYQmwmxPryK5FW7yGTxwwClPBv7jywSK3axGLKRX
# w/rN9vbyjmh9zLtavCM4Vy1b77pCWg1cJXQAZ7yQPEQ2UzYL4TgQ140jmqBcPrM+
# bMkA6TduQZtREW7iSxKy+KRNwO8FMIxyxRlDoHx2sVcN9kgbAA4dqsYFeqkqceAR
# 7Hh9McRTfXiH3cPVRkTcOyzzJ1eZ0nkwnOVXowhy3CaiIo3dwYwfz4tz+z8f7hHt
# KBnv5DcTG6UGIdwLad2UlcaL2m0RuUsaQzwaKMc5Yx/L1iXahoPHsnzzYqw7ZkaC
# JUmmS4bLRg/Fzi25iOaAaIVLO0n0dP0LVbCwiA8kxLrlARvwKmJxrir0RFMApIfX
# oFsJqFP5XjqSU9tPVj8rmacQaDVawRX069Y66cgGL627kVc16z46cerP+lokt7z5
# FW5vRLvqjb2hggMmMIIDIgYJKoZIhvcNAQkGMYIDEzCCAw8CAQEwfTBpMQswCQYD
# VQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lD
# ZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUg
# Q0ExAhAKgO8YS43xBYLRxHanlXRoMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcN
# AQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjYwNTI2MDQwNjQ2WjAv
# BgkqhkiG9w0BCQQxIgQgim6zqMlrWsQjLt4qnEu258j7C1DpXDlXnZFxOmBhJ98w
# DQYJKoZIhvcNAQEBBQAEggIAwmmt+yobkQT2gO/hN9dzlZXbWalCccfI4q1QkTSQ
# Uef2kr5oebu0MzNvLzmy5P725bUfW7Isqcq5lPrAwk3IL6Mgq6XTgweRgpqoHZrX
# tl3cG13BXFcXKG1Q9ppibtkdI37e/2g16olH/wo4lZSRg+wvmCXUgjRMHSmYF+Rw
# aHZ3hlaDJqXgZnWVVENqCeCdJeJIG8VD/iP7lTH425u17NCMl+8+sj0Wc/J1uJwv
# kI4zvaTWCehJ25WNSagbtFNC/Z+h0aL02GlgVG3CMf6Js1NqP8M7GzXTZwFX9tg4
# I04naKsYetoBWY6J1UfDCd0OYYSSQIB6Hi5j+BS7o9kVDua/BDN/+iYSDRDI5Xew
# fDaHSu0G0Sn/7lXY64gRX4qgGpwssRJg8S7zBbgYLkWAKtk/WDnAY5osaySo3OoR
# 2N7XKktIcNdoF5Sgp56GKXX4LFzjmhsD+bEzsMQNEYa/OjomX76RJ+y9rtvNEBYB
# ltcn5W2er1UitAxl4WCTQggnvSL8IfjyUF+BynCA44v4iHsnvRWNLzwodT5LJ6Ku
# G+fUEK2u6xOoEQiq5i5FGz4D1nKGVeHOTfgqyysRXzaYc+iYoi1gmoP+WhHRLIf8
# haUYcJm5i1HOZoGZfAyYtaKpFyCW4j/xxlg0kmybaa/kC1SK5rDlJB3F9jSZj0UI
# rIA=
# SIG # End signature block