Unable to create a FileZilla/SFTP entry with Devolutions.PowerShell for DVLS

Resolved

Unable to create a FileZilla/SFTP entry with Devolutions.PowerShell for DVLS

avatar

Hello,
I am currently migrating automation scripts from Remote Desktop Manager PowerShell to Devolutions.PowerShell for DVLS.

I managed to create SSH and Web entries using New-DSEntryBase. Now, I am trying to automatically create FileZilla SFTP entries using New-DSEntryBase.

I successfully create the entry in DVLS, but when opening it from the Web UI or from RDM app, the entry is incomplete. There is no host, no username, no password...

What I already tested:

  • Creating entries manually in DVLS
  • Exporting existing entries using Get-DSEntry
  • Rebuilding the JSON payload manually
  • Using:
    • connectionType = "FTP" or "9"
    • connectionSubType = "FileZilla"
    • protocol/host/port/username/password fields
  • Cloning existing entries

However, exported FileZilla entries seem incomplete:

  • host is missing
  • username is missing
  • provider-specific settings are not visible
  • only minimal data is returned


I also tried using New-DSEntryBase -FromRDMConnection and integrate the input configuration that is used in my functional RDM PowerShell script but it fails because the cmdlet expects:

Devolutions.RemoteDesktopManager.Business.Entities.ConnectionInfoEntity

while New-RDMSession returns:

RemoteDesktopManager.PowerShellModule.PSOutputObject.PSConnection


My goal is to fully automate the creation of FileZilla SFTP entries in DVLS from PowerShell.
Could you please provide:

  1. The correct JSON schema for FileZilla SFTP entries
  2. The required connectionType / connectionSubType values
  3. The recommended way to automate FileZilla entry creation in DVLS
  4. Whether some provider-specific data is intentionally hidden from Get-DSEntry


Thank you.

Here is my non-functional JSON configuration :

$body = @{
    name = "SFTP - Test 01"
    group = "FOLDER01"
    vaultID = "00000000-0000-0000-0000-000000000000"
    connectionType = "9"
    data = @{
        host = "10.1.1.1"
        username = "sftpuser"
        protocol = 1
        passwordItem = @{
            sensitiveData = "MyPassword123!"
        }
    }
}

All Comments (6)

avatar

Hello dsi1,

Thank you for contacting the Devolutions support team.

We do not recommend creating FileZilla/SFTP entries by manually inventing a New-DSEntryBase -JsonBody schema.
FileZilla is an add-on/provider-style entry, and the provider-specific data is not reliably represented by simple fields such as data.host or data.username.

Since you want to use DS cmdlets only, the supported workaround is to create one valid FileZilla entry as a template, retrieve it with Get-DSEntry -AsRDMConnection, modify the returned ConnectionInfoEntity, and then create the new entry with New-DSEntryBase -FromRDMConnection.

This avoids the New-RDMSession / PSConnection mismatch and preserves the FileZilla-specific internal fields.

Here is a script template to use.
Note that I didn't test this script, and I suggest trying it in a test vault or a test environment before executing it for the production database.

#requires -Modules Devolutions.PowerShell

function ConvertTo-PlainText {
    param(
        [Parameter(Mandatory)]
        [System.Security.SecureString] $SecureString
    )

    $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString)

    try {
        [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
    }
    finally {
        if ($bstr -ne [IntPtr]::Zero) {
            [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
        }
    }
}

function Set-NotePropertyValue {
    param(
        [Parameter(Mandatory)]
        [object] $InputObject,

        [Parameter(Mandatory)]
        [string] $Name,

        [Parameter(Mandatory)]
        [AllowNull()]
        [object] $Value
    )

    if ($InputObject.PSObject.Properties[$Name]) {
        $InputObject.$Name = $Value
    }
    else {
        $InputObject | Add-Member -MemberType NoteProperty -Name $Name -Value $Value -Force
    }
}

function New-DSFileZillaSftpEntry {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [guid] $TemplateEntryId,

        [Parameter(Mandatory)]
        [guid] $VaultId,

        [Parameter(Mandatory)]
        [string] $Name,

        [Parameter(Mandatory)]
        [string] $Group,

        [Parameter(Mandatory)]
        [string] $HostName,

        [Parameter(Mandatory)]
        [string] $Username,

        [Parameter(Mandatory)]
        [System.Security.SecureString] $Password
    )

    # 1. Fetch an existing working FileZilla entry as a full RDM connection entity.
    $connection = Get-DSEntry `
        -EntryId $TemplateEntryId `
        -VaultID $VaultId `
        -AsRDMConnection

    if (-not $connection) {
        throw "Template entry '$TemplateEntryId' was not found."
    }

    if (-not $connection.PSObject.Properties['AddOn'] -or -not $connection.AddOn) {
        throw "The template entry does not expose AddOn settings. Use a working FileZilla AddOn entry as the template."
    }

    # 2. Modify the full connection object.
    $connection.Name = $Name
    $connection.Group = $Group

    # Keep the template's ConnectionType and ConnectionSubType.
    # For FileZilla AddOn entries, ConnectionSubType must remain the FileZilla add-on identifier.
    $connection.AddOn.Host = $HostName
    $connection.AddOn.Username = $Username

    # If the object exposes a repository/vault property, keep it aligned.
    foreach ($propertyName in @('RepositoryID', 'RepositoryId', 'VaultID', 'VaultId')) {
        if ($connection.PSObject.Properties[$propertyName]) {
            $connection.$propertyName = $VaultId
            break
        }
    }

    # 3. Create the new entry using DS cmdlet only.
    $createdConnection = New-DSEntryBase -FromRDMConnection $connection

    $createdEntryId = $createdConnection.ID
    if (-not $createdEntryId) {
        $createdEntryId = $createdConnection.Id
    }

    if (-not $createdEntryId) {
        throw "The entry was created, but the new entry ID could not be resolved from the returned object."
    }

    # 4. Update the password using DS JSON update pattern.
    # Update-DSEntryBase documentation shows password updates through data.passwordItem.SensitiveData.
    $plainPassword = ConvertTo-PlainText -SecureString $Password

    try {
        $createdEntry = Get-DSEntry -EntryId $createdEntryId -VaultID $VaultId

        if (-not $createdEntry.PSObject.Properties['data'] -or -not $createdEntry.data) {
            throw "The created entry does not expose a data object through Get-DSEntry."
        }

        if (-not $createdEntry.data.PSObject.Properties['passwordItem'] -or -not $createdEntry.data.passwordItem) {
            Set-NotePropertyValue -InputObject $createdEntry.data -Name 'passwordItem' -Value ([pscustomobject]@{})
        }

        Set-NotePropertyValue `
            -InputObject $createdEntry.data.passwordItem `
            -Name 'SensitiveData' `
            -Value $plainPassword

        $jsonBody = $createdEntry | ConvertTo-Json -Depth 25
        Update-DSEntryBase -JsonBody $jsonBody | Out-Null
    }
    finally {
        $plainPassword = $null
    }

    # 5. Return the created entry.
    Get-DSEntry -EntryId $createdEntryId -VaultID $VaultId
}

# Example usage
$password = Read-Host "SFTP password" -AsSecureString

New-DSFileZillaSftpEntry `
    -TemplateEntryId '11111111-1111-1111-1111-111111111111' `
    -VaultId '00000000-0000-0000-0000-000000000000' `
    -Name 'SFTP - Test 01' `
    -Group 'FOLDER01' `
    -HostName '10.1.1.1' `
    -Username 'sftpuser' `
    -Password $password


Best regards,

Patrick Ouimet

avatar

Sorry, but it's not working. I'm getting almost no value from the template entry. In particular, there's no "AddOn" property. I only get :

  • ConnectionInfo
  • SecurityGroupName
  • TypeDescription
  • TypeId


Just to reiterate, I can create the entry, but it doesn't contain some of the data I need:

  • Host IP address
  • Username
  • Password
  • SFTP protocol
  • Home directory
avatar

Hello,

Thank you for your feedback.

Have you tried creating the entry in the DVLS web UI and fetching it from PowerShell as a reverse-engineering method to understand the content of this object, as my colleague Patrick mentioned? The supported workaround is to create one valid FileZilla entry as a template, retrieve it with Get-DSEntry -AsRDMConnection, modify the returned ConnectionInfoEntity, and then create the new entry with New-DSEntryBase -FromRDMConnection.

Properties are maybe unavailable because the FileZilla entry type is an add-on/provider-style entry, and the provider-specific data is not reliably represented by simple fields such as data.host or data.username.

Let us know if that helps.

Best regards,

Érica Poirier

avatar

Hello,

I just want to share the work I've been doing on the script since my last response.

Configuring an entry that is not supported by the web UI is quite complex.
However, I was able to set the SFTP protocol and the host in the entry by reverse engineering the process.

Note that the password is still missing in the entry.
I can provide a way to change the credential connection type to Inherited or Linked to the vault.

Also, to ensure that it could be easier for the furtur to set this entry with DS cmdlet, i suggest to open a feature request thread to support this entry type by the web UI.

I will provide the new script next Tuesday after a full test in my environment.

Best regards,

Patrick Ouimet

avatar

Hello dsi1,

Here is the script:

#requires -Modules Devolutions.PowerShell

<#
.SYNOPSIS
    Clone a FileZilla/WinSCP SFTP template entry in Devolutions Server and link it
    to an existing credential entry by CredentialConnectionID.

.DESCRIPTION
    This script:
      - Authenticates to Devolutions Server with an application identity.
      - Fetches a source FTP/SFTP template as an RDM ConnectionInfoEntity.
      - Clones the entry into the target vault/folder.
      - Updates the connection XML with:
            <CredentialConnectionID>1310CF82-6FAB-4B7A-9EEA-3E2E451CA2CF</CredentialConnectionID>
      - Removes embedded username/password/domain XML fields from the cloned entry.

.NOTES
    If the linked credential is in another vault, pass -CredentialRepositoryId.
    If it is in the same vault, do not pass -CredentialRepositoryId.

    IMPORTANT:
    Do not hardcode application keys in production scripts.
#>

# ============================================================================
# 0. Devolutions Server session - Application identity
# ============================================================================

#requires -Modules Devolutions.PowerShell

# ============================================================================
# 0. Devolutions Server session (Application identity)
# ============================================================================
$env:DS_URL      = "<DVLS URL>"
$env:DS_USER     = "<App Key>"
$env:DS_PASSWORD = '<App Secret>'

$DSApplicationId  = $env:DS_USER
$DSApplicationKey = $env:DS_PASSWORD
$DSBaseUri        = $env:DS_URL

$DSSecureKey = ConvertTo-SecureString -String $DSApplicationKey -AsPlainText -Force
$DSCreds     = [pscredential]::new($DSApplicationId, $DSSecureKey)

$null = New-DSSession -Credential $DSCreds -BaseURI $DSBaseUri -AsApplication

# ============================================================================
# 1. Helpers
# ============================================================================

function Set-XmlElementValue {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Xml.XmlElement] $Parent,

        [Parameter(Mandatory)]
        [string] $TagName,

        [Parameter(Mandatory)]
        [AllowEmptyString()]
        [string] $Value
    )

    $node = $Parent.SelectSingleNode($TagName)

    if (-not $node) {
        $node = $Parent.OwnerDocument.CreateElement($TagName)
        $Parent.AppendChild($node) | Out-Null
    }

    $node.InnerText = $Value
}

function Remove-XmlElementIfPresent {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Xml.XmlElement] $Parent,

        [Parameter(Mandatory)]
        [string] $TagName
    )

    $node = $Parent.SelectSingleNode($TagName)

    if ($node) {
        $Parent.RemoveChild($node) | Out-Null
    }
}

function Clear-EmbeddedCredentialXml {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Xml.XmlElement] $Parent
    )

    foreach ($tagName in @(
        'UserName',
        'Username',
        'Domain',
        'Password',
        'SafePassword',
        'MnemonicPassword',
        'PrivateKeyPassphrase',
        'SafePrivateKeyPassphrase'
    )) {
        Remove-XmlElementIfPresent -Parent $Parent -TagName $tagName
    }
}

# ============================================================================
# 2. Clone function - linked credential only
# ============================================================================

function New-DSFileZillaSftpEntry {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)]
        [guid] $TemplateEntryId,

        [Parameter(Mandatory)]
        [guid] $VaultId,

        [Parameter(Mandatory)]
        [string] $Name,

        [Parameter(Mandatory)]
        [string] $Group,

        [Parameter(Mandatory)]
        [string] $HostName,

        [ValidateSet('SFtp', 'Ftp', 'Ftps', 'FtpEs')]
        [string] $Protocol = 'SFtp',

        [ValidateSet('Filezilla', 'WinSCP', 'Native')]
        [string] $Application = 'Filezilla',

        [Parameter(Mandatory)]
        [guid] $CredentialConnectionId,

        # Optional. Use only if the credential entry is stored in another vault.
        [guid] $CredentialRepositoryId
    )

    # Fetch the template as a full RDM connection object.
    $wrapper = Get-DSEntry -EntryId $TemplateEntryId -AsRDMConnection -ErrorAction Stop

    if (-not $wrapper -or -not $wrapper.ConnectionInfo) {
        throw "Source entry '$TemplateEntryId' was not found, or did not return a ConnectionInfo object."
    }

    $conn = $wrapper.ConnectionInfo

    if ([string]$conn.ConnectionType -ne 'Ftp') {
        throw "Source ConnectionType is '$($conn.ConnectionType)', expected 'Ftp'."
    }

    if (-not $conn.Data) {
        throw "Source entry has no Data XML payload."
    }

    $newId = [guid]::NewGuid()

    # Mirror identity fields on the wrapper.
    $conn.Name         = $Name
    $conn.Group        = $Group
    $conn.ID           = $newId.ToString()
    $conn.RepositoryID = [string]$VaultId

    if ($conn.PSObject.Properties['GroupMain']) {
        $conn.GroupMain = $Group
    }

    # Edit the connection XML.
    [xml] $xml = $conn.Data

    $root = $xml.Connection
    if (-not $root) {
        throw "Source Data XML is missing a <Connection> root element."
    }

    Set-XmlElementValue -Parent $root -TagName 'ID'    -Value $newId.ToString()
    Set-XmlElementValue -Parent $root -TagName 'Name'  -Value $Name
    Set-XmlElementValue -Parent $root -TagName 'Group' -Value $Group

    $ftp = $root.SelectSingleNode('Ftp')
    if (-not $ftp) {
        $ftp = $xml.CreateElement('Ftp')
        $root.AppendChild($ftp) | Out-Null
    }

    Set-XmlElementValue -Parent $ftp -TagName 'Host'        -Value $HostName
    Set-XmlElementValue -Parent $ftp -TagName 'Protocol'    -Value $Protocol
    Set-XmlElementValue -Parent $ftp -TagName 'Application' -Value $Application

    # Linked credential configuration.
    Set-XmlElementValue -Parent $root -TagName 'CredentialConnectionID' `
        -Value ($CredentialConnectionId.ToString().ToUpperInvariant())

    if ($PSBoundParameters.ContainsKey('CredentialRepositoryId') -and $CredentialRepositoryId -ne [guid]::Empty) {
        Set-XmlElementValue -Parent $root -TagName 'CredentialRepositoryID' `
            -Value ($CredentialRepositoryId.ToString().ToUpperInvariant())
    }
    else {
        Remove-XmlElementIfPresent -Parent $root -TagName 'CredentialRepositoryID'
    }

    # Remove inline credential fields from the cloned entry.
    Clear-EmbeddedCredentialXml -Parent $root
    Clear-EmbeddedCredentialXml -Parent $ftp

    $conn.Data = $xml.OuterXml

    if ($VerbosePreference -ne 'SilentlyContinue') {
        $dumpPath = Join-Path $env:TEMP "ds-clone-$($newId).xml"
        $xml.Save($dumpPath)
        Write-Verbose "Outgoing XML written to: $dumpPath"
    }

    if (-not $PSCmdlet.ShouldProcess(
            "'$Name' in vault $VaultId",
            "Create new FileZilla/WinSCP $Protocol entry from template $TemplateEntryId using linked credential $CredentialConnectionId"
        )) {
        return
    }

    # Create the new entry from the modified ConnectionInfoEntity.
    $createdResp = New-DSEntryBase -FromRDMConnection $conn -ErrorAction Stop

    # Resolve the created entry ID. Response shape can vary by version.
    $createdEntryId = $null

    if ($createdResp.PSObject.Properties['ConnectionInfo'] -and $createdResp.ConnectionInfo.ID) {
        $createdEntryId = $createdResp.ConnectionInfo.ID
    }

    if (-not $createdEntryId) {
        foreach ($propertyName in 'ID', 'Id', 'id') {
            if ($createdResp.PSObject.Properties[$propertyName] -and $createdResp.$propertyName) {
                $createdEntryId = $createdResp.$propertyName
                break
            }
        }
    }

    if (-not $createdEntryId -and $createdResp.PSObject.Properties['data'] -and $createdResp.data) {
        foreach ($propertyName in 'ID', 'Id', 'id') {
            if ($createdResp.data.PSObject.Properties[$propertyName] -and $createdResp.data.$propertyName) {
                $createdEntryId = $createdResp.data.$propertyName
                break
            }
        }
    }

    if (-not $createdEntryId) {
        $createdEntryId = $newId
    }

    # Return a clean result object.
    [pscustomobject]@{
        EntryId                = [guid]$createdEntryId
        Name                   = $Name
        Group                  = $Group
        Host                   = $HostName
        Protocol               = $Protocol
        Application            = $Application
        CredentialMode         = 'Linked'
        CredentialConnectionId = [guid]$CredentialConnectionId
        CredentialRepositoryId = if ($PSBoundParameters.ContainsKey('CredentialRepositoryId') -and $CredentialRepositoryId -ne [guid]::Empty) {
            [guid]$CredentialRepositoryId
        }
        else {
            $null
        }
        VaultId                = [guid]$VaultId
        SourceEntryId          = [guid]$TemplateEntryId
    }
}

# ============================================================================
# 3. Example usage - linked credential only
# ============================================================================

New-DSFileZillaSftpEntry `
    -TemplateEntryId        '<EntryID>' `
    -VaultId                'VaultID' `
    -Name                   '<New Entry Name>' `
    -Group                  'Folder Name' `
    -HostName               '<Host Name or IP>' `
    -CredentialConnectionId '1310CF82-6FAB-4B7A-9EEA-3E2E451CA2CF' `
    -Protocol               'SFtp' `
    -Application            'Filezilla' `
    -Verbose


With this script, you should be able to adapt it to your environment and add the necessary properties.

Best regards,

Patrick Ouimet

avatar

Hello. I can confirm that the script works. I'm not sure how I'll be able to integrate it, but I should be able to manage.

Thank you very much for your help.

Ends in 6 days