diff --git a/Dynamic Folder/Bitwarden/Bitwarden (PowerShell).rdfe b/Dynamic Folder/Bitwarden/Bitwarden (PowerShell).rdfe index 3439d98..cc27cef 100644 --- a/Dynamic Folder/Bitwarden/Bitwarden (PowerShell).rdfe +++ b/Dynamic Folder/Bitwarden/Bitwarden (PowerShell).rdfe @@ -3,7 +3,7 @@ "Objects": [ { "Notes": "\r\n\r\n\t\r\n\t\t\r\n\t\t\r\n\t\t\r\n\t\r\n\t\r\n\t\t

\r\n\t\t\t Bitwarden Dynamic Folder sample with Powershell

\r\n\t\t

Version: 1.0.0
Author: Nicolas Grimler

This Dynamic Folder sample allows you to import credentials from Bitwarden. The Bitwarden CLI client is required and the full executable path where it is installed must be configured in the "Custom Properties" section. Also, your Bitwarden login details must be provided in the "Credentials" section.

It use the Bitwarden User API to login and the master password to unlock the vault. Please read https://bitwarden.com/help/personal-api-key/ to know how to get your personal API Key.
If you don't want to use an API Key, please ensure that you are already logged in using the bw.exe CLI tool as the script will not handle the TOTP 2FA handshake.

At the moment, only credentials and secure notes are collected. The folder structure is as presented in Bitwarden (Folder, Folder/Subfolder, ...). Support for full directory structure may be implemented in future version.

\r\n\t\t\tRequirements

\r\n\t\t\r\n\t\t

 

\r\n\t\t\tSetup

\r\n\t\t\r\n\t\t

 

Important note:

In the configuration of the interpreter used to run the script, check the box "Do not load the PowerShell profile" as it may otherwise add unwanted messages invalidating the JSON output and causing errors.

\r\n\r\n", - "Script": "# Env config\r\n$global:OutputEncoding = New-Object Text.Utf8Encoding -ArgumentList (,$false) # BOM-less\r\n[Console]::OutputEncoding = $global:OutputEncoding\r\n\r\n# Bitwarden access config\r\n$Bitwarden = ( New-Object PSObject |\r\n Add-Member -PassThru NoteProperty exec_path '$CustomProperty.BitWardenCLIExecutable$' |\r\n Add-Member -PassThru NoteProperty serverUrl '$CustomProperty.BitWardenServerURL$' |\r\n Add-Member -PassThru NoteProperty clientId '$CustomProperty.APIClientID$' |\r\n Add-Member -PassThru NoteProperty clientSecret '$CustomProperty.APIClientSecret$' |\r\n Add-Member -PassThru NoteProperty password '$CustomProperty.AccountPassword$' |\r\n Add-Member -PassThru NoteProperty session '' )\r\n\r\n# Structures\r\n$final = @{ Objects = @(@{ Type = \"Folder\"; ID = \"personal\"; Name = \"Personal Vault\"; IconName = \"Flat/Objects/User Record\"; Objects = @(); }); }\r\n\r\n# Functions\r\nfunction Get-VaultItems {\r\n [CmdletBinding()]\r\n param (\r\n [Parameter(Mandatory=$false)]\r\n [string]$folderId = \"\",\r\n [Parameter(Mandatory=$false)]\r\n [string]$collectionId = \"\"\r\n )\r\n\r\n if ($folderId -eq \"\" -and $collectionId -eq \"\") { Write-Error \"Folder ID or Collection ID needed\"}\r\n\r\n if ($folderId -ne \"\" -and $collectionId -eq \"\") {\r\n $tmpItems = (Invoke-Command -ScriptBloc { & \"$($Bitwarden.exec_path)\" list items --folderid $folderId --session \"$($Bitwarden.session)\"}) | ConvertFrom-Json\r\n } elseif ($folderId -eq \"\" -and $collectionId -ne \"\") {\r\n $tmpItems = (Invoke-Command -ScriptBloc { & \"$($Bitwarden.exec_path)\" list items --collectionid $collectionId --session \"$($Bitwarden.session)\"}) | ConvertFrom-Json\r\n } else {\r\n Write-Error \"Only one of FolderId or CollectionId allowed\"\r\n }\r\n $items = [array]@()\r\n foreach ($item in $tmpItems) {\r\n # Skip shared items with an organization to prevent duplicates\r\n if ($folderid -ne \"\" -and $null -ne $item.organizationid) { continue }\r\n\r\n # Parse item of type Login/Secure Note only\r\n switch ($item.type) {\r\n \"1\" { # Login\r\n $row = \"\" | Select-Object Type,ID,Name,Notes,Favorite,Username,Password,URL,CustomProperties\r\n $row.Type = \"Credential\"\r\n $row.ID = $item.id\r\n $row.Name = $item.name\r\n if ($null -ne $item.notes) {\r\n $row.Notes = $item.notes.Replace(\"`r`n\", \"
\").Replace(\"`r\", \"
\").Replace(\"`n\", \"
\")\r\n }\r\n if ($item.favorite -eq \"true\") { $row.Favorite = $true } else { $row.Favorite = $false }\r\n $row.Username = $item.login.username\r\n $row.Password = $item.login.password\r\n if ($item.login.uris.Count -gt 0) {\r\n $row.URL = $item.login.uris[0].uri\r\n }\r\n $row.CustomProperties = [array]@()\r\n if ($item.fields.count -gt 0) {\r\n $itemFields = [array]@()\r\n $fieldIndex = 0\r\n foreach ($field in $item.fields) {\r\n $frow = \"\" | Select-Object Type,Name,Value\r\n switch ($field.type) {\r\n \"0\" { $frow.Type = \"Text\" }\r\n \"1\" { $frow.Type = \"Protected\" }\r\n \"2\" { $frow.Type = \"YesNo\" }\r\n }\r\n if ($null -eq $field.name) {\r\n $frow.Name = \"UnnamedField$($fieldIndex)\"\r\n $fieldIndex++\r\n } else {\r\n $frow.Name = $field.name\r\n }\r\n $frow.Value = $field.value\r\n\r\n $itemFields += $frow\r\n }\r\n \r\n $row.CustomProperties = $itemFields\r\n }\r\n $items += $row\r\n }\r\n \"2\" { # Secure Note\r\n $row = \"\" | Select-Object Type,ID,Name,Notes,TemplateID,CustomProperties\r\n $row.Type = \"Information\"\r\n $row.ID = $item.id\r\n $row.Name = $item.name\r\n if ($null -ne $item.notes) {\r\n $row.Notes = $item.notes.Replace(\"`r`n\", \"
\").Replace(\"`r\", \"
\").Replace(\"`n\", \"
\")\r\n }\r\n $row.TemplateID = \"Custom\"\r\n $row.CustomProperties = @()\r\n $itemFields = [array]@()\r\n if ($item.fields.count -gt 0) {\r\n $fieldIndex = 0\r\n foreach ($field in $item.fields) {\r\n $frow = \"\" | Select-Object Type,Name,Value\r\n switch ($field.type) {\r\n \"0\" { $frow.Type = \"Text\" }\r\n \"1\" { $frow.Type = \"Protected\" }\r\n \"2\" { $frow.Type = \"YesNo\" }\r\n }\r\n if ($null -eq $field.name) {\r\n $frow.Name = \"UnnamedField$($fieldIndex)\"\r\n $fieldIndex++\r\n } else {\r\n $frow.Name = $field.name\r\n }\r\n $frow.Value = $field.value\r\n\r\n $itemFields += $frow\r\n }\r\n } else {\r\n $itemFields += @{ Type = \"Header\"; Name = \"See notes for details\"; Value = \"See notes for details\"; }\r\n }\r\n $row.CustomProperties = $itemFields\r\n $items += $row\r\n }\r\n }\r\n }\r\n\r\n return $items\r\n}\r\n\r\n# Get Vault status\r\n$status = (Invoke-Command -ScriptBloc { & \"$($Bitwarden.exec_path)\" status }) | ConvertFrom-Json\r\n\r\nif ($null -ne $status) {\r\n switch ($status.status) {\r\n \"unauthenticated\" {\r\n if ($null -eq $status.serverUrl -or $status.serverUrl -ne $Bitwarden.serverUrl) {\r\n # Vault not configured, configure server\r\n [void](Invoke-Command -ScriptBlock { & \"$($Bitwarden.exec_path)\" config server \"$($Bitwarden.serverUrl)\" })\r\n }\r\n\r\n # Prepare Vault login using API key\r\n $env:BW_CLIENTID = $Bitwarden.clientId\r\n $env:BW_CLIENTSECRET = $Bitwarden.clientSecret\r\n $env:BW_PASSWORD = $Bitwarden.password\r\n [void](Invoke-Command -ScriptBlock { & \"$($Bitwarden.exec_path)\" login --apikey})\r\n\r\n # Unlock Vault using password\r\n $Bitwarden.session = Invoke-Command -ScriptBlock { & \"$($Bitwarden.exec_path)\" unlock --passwordenv BW_PASSWORD --raw }\r\n\r\n # Clear env variables\r\n Remove-Item -Path Env:\\BW_*\r\n }\r\n \"locked\" {\r\n # Vault is locked, unlock it with password\r\n $env:BW_PASSWORD = $Bitwarden.password\r\n $Bitwarden.session = Invoke-Command -ScriptBlock { & \"$($Bitwarden.exec_path)\" unlock --passwordenv BW_PASSWORD --raw }\r\n\r\n # Clear env variables\r\n Remove-Item -Path Env:\\BW_*\r\n }\r\n }\r\n} else {\r\n Write-Error \"Unable to get Vault status\"\r\n}\r\n\r\nif ($null -ne $Bitwarden.session) {\r\n # Sync Vault to latest version from server\r\n [void](Invoke-Command -ScriptBlock { & \"$($Bitwarden.exec_path)\" sync --session \"$($Bitwarden.session)\"})\r\n\r\n # Get and parse Personal Vault folders\r\n $tmpFolders = (Invoke-Command -ScriptBloc { & \"$($Bitwarden.exec_path)\" list folders --session \"$($Bitwarden.session)\"}) | ConvertFrom-Json\r\n\r\n foreach ($folder in $tmpFolders) {\r\n if ($null -ne $folder.id) {\r\n $tF = @{ Type = \"Folder\"; ID = $folder.id; Name = $folder.name; Objects = [array]@(Get-VaultItems -folderId $folder.id); }\r\n if ($tF.Objects.Count -ne 0) { $final.Objects[0].Objects += $tF; $tF = $null }\r\n } else {\r\n # Add default folder\r\n $tF = @{ Type = \"Folder\"; ID = \"nofolder\"; Name = \"No folder\"; Objects = [array]@(Get-VaultItems -folderId null); }\r\n if ($tF.Objects.Count -ne 0) { $final.Objects[0].Objects += $tF; $tF = $null }\r\n }\r\n }\r\n\r\n # Get and parse Organisations and Collections\r\n $organizations = (Invoke-Command -ScriptBloc { & \"$($Bitwarden.exec_path)\" list organizations --session \"$($Bitwarden.session)\"}) | ConvertFrom-Json\r\n\r\n foreach ($org in $organizations) {\r\n # Get collections for the organization\r\n $collections = (Invoke-Command -ScriptBloc { & \"$($Bitwarden.exec_path)\" list collections --organizationid $org.id --session \"$($Bitwarden.session)\"}) | ConvertFrom-Json\r\n $tOrgCollections = [array]@()\r\n foreach ($coll in $collections) {\r\n $tF = @{ Type = \"Folder\"; ID = $coll.id; Name = $coll.name; IconName = \"Flat/Software/Tree\"; Objects = [array]@(Get-VaultItems -collectionId $coll.id); }\r\n if ($tF.Objects.Count -ne 0) { $tOrgCollections += $tF; $tF = $null }\r\n }\r\n if ($tOrgCollections.Count -gt 0) {\r\n # Create organization folder\r\n $final.Objects += @{ Type = \"Folder\"; ID = $org.id; Name = $org.name; IconName = \"Flat/Money/Bank\"; Objects = $tOrgCollections; }\r\n }\r\n }\r\n}\r\n\r\n# Adapt JSON output for PowerShell version\r\nif ($PSVersionTable.PSVersion -ge '6.2') {\r\n ConvertTo-Json -InputObject $final -Depth 100 -EscapeHandling EscapeHtml |Out-file \".\\bitwarden_output.json\" -Force\r\n ConvertTo-Json -InputObject $final -Depth 100 -EscapeHandling EscapeHtml\r\n} else {\r\n ConvertTo-Json -InputObject $final -Depth 100 |Out-file \".\\bitwarden_output.json\" -Force\r\n ConvertTo-Json -InputObject $final -Depth 100\r\n}\r\n", + "Script": "# Env config\r\n$global:OutputEncoding = New-Object Text.Utf8Encoding -ArgumentList (,$false) # BOM-less\r\n[Console]::OutputEncoding = $global:OutputEncoding\r\n$PSStyle.OutputRendering = 'PlainText'\r\n\r\n# Bitwarden access config\r\n$Bitwarden = ( New-Object PSObject |\r\n Add-Member -PassThru NoteProperty exec_path '$CustomProperty.BitWardenCLIExecutable$' |\r\n Add-Member -PassThru NoteProperty serverUrl '$CustomProperty.BitWardenServerURL$' |\r\n Add-Member -PassThru NoteProperty clientId '$CustomProperty.APIClientID$' |\r\n Add-Member -PassThru NoteProperty clientSecret '$CustomProperty.APIClientSecret$' |\r\n Add-Member -PassThru NoteProperty password '$CustomProperty.AccountPassword$' |\r\n Add-Member -PassThru NoteProperty session '' )\r\n\r\n# Check bw.exe path validity\r\nif (!(Test-Path -Path \"$($Bitwarden.exec_path)\" -PathType Leaf)) {\r\n Write-Error -Message \"Bitwarden CLI utility not found at specified path. Please check CLI utility path in Custom Properties.\" -ErrorAction Stop\r\n}\r\n\r\n# Structures\r\n$final = @{ Objects = @(@{ Type = \"Folder\"; ID = \"personal\"; Name = \"Personal Vault\"; IconName = \"Flat/Objects/User Record\"; Objects = @(); }); }\r\n\r\n# Functions\r\nfunction Get-VaultItems {\r\n [CmdletBinding()]\r\n param (\r\n [Parameter(Mandatory=$false)]\r\n [string]$folderId = \"\",\r\n [Parameter(Mandatory=$false)]\r\n [string]$collectionId = \"\"\r\n )\r\n\r\n if ($folderId -eq \"\" -and $collectionId -eq \"\") { Write-Error -Message \"Folder ID or Collection ID needed, none provided.\" -ErrorAction Stop }\r\n\r\n if ($folderId -ne \"\" -and $collectionId -eq \"\") {\r\n $tmpItems = (Invoke-Command -ScriptBloc { & \"$($Bitwarden.exec_path)\" list items --folderid $folderId --session \"$($Bitwarden.session)\"}) | ConvertFrom-Json\r\n } elseif ($folderId -eq \"\" -and $collectionId -ne \"\") {\r\n $tmpItems = (Invoke-Command -ScriptBloc { & \"$($Bitwarden.exec_path)\" list items --collectionid $collectionId --session \"$($Bitwarden.session)\"}) | ConvertFrom-Json\r\n } else {\r\n Write-Error -Message \"Either FolderId or CollectionId are needed, not both.\" -ErrorAction Stop\r\n }\r\n $items = [array]@()\r\n foreach ($item in $tmpItems) {\r\n # Skip shared items with an organization to prevent duplicates\r\n if ($folderid -ne \"\" -and $null -ne $item.organizationid) { continue }\r\n\r\n # Parse item of type Login/Secure Note only\r\n switch ($item.type) {\r\n \"1\" { # Login\r\n $row = \"\" | Select-Object Type,ID,Name,Notes,Favorite,Username,Password,URL,CustomProperties\r\n $row.Type = \"Credential\"\r\n $row.ID = $item.id\r\n $row.Name = $item.name\r\n if ($null -ne $item.notes) {\r\n $row.Notes = $item.notes.Replace(\"`r`n\", \"
\").Replace(\"`r\", \"
\").Replace(\"`n\", \"
\")\r\n }\r\n if ($item.favorite -eq \"true\") { $row.Favorite = $true } else { $row.Favorite = $false }\r\n $row.Username = $item.login.username\r\n $row.Password = $item.login.password\r\n if ($item.login.uris.Count -gt 0) {\r\n $row.URL = $item.login.uris[0].uri\r\n }\r\n $row.CustomProperties = [array]@()\r\n if ($item.fields.count -gt 0) {\r\n $itemFields = [array]@()\r\n $fieldIndex = 0\r\n foreach ($field in $item.fields) {\r\n $frow = \"\" | Select-Object Type,Name,Value\r\n switch ($field.type) {\r\n \"0\" { $frow.Type = \"Text\" }\r\n \"1\" { $frow.Type = \"Protected\" }\r\n \"2\" { $frow.Type = \"YesNo\" }\r\n }\r\n if ($null -eq $field.name) {\r\n $frow.Name = \"UnnamedField$($fieldIndex)\"\r\n $fieldIndex++\r\n } else {\r\n $frow.Name = $field.name\r\n }\r\n $frow.Value = $field.value\r\n\r\n $itemFields += $frow\r\n }\r\n \r\n $row.CustomProperties = $itemFields\r\n }\r\n $items += $row\r\n }\r\n \"2\" { # Secure Note\r\n $row = \"\" | Select-Object Type,ID,Name,Notes,TemplateID,CustomProperties\r\n $row.Type = \"Information\"\r\n $row.ID = $item.id\r\n $row.Name = $item.name\r\n if ($null -ne $item.notes) {\r\n $row.Notes = $item.notes.Replace(\"`r`n\", \"
\").Replace(\"`r\", \"
\").Replace(\"`n\", \"
\")\r\n }\r\n $row.TemplateID = \"Custom\"\r\n $row.CustomProperties = @()\r\n $itemFields = [array]@()\r\n if ($item.fields.count -gt 0) {\r\n $fieldIndex = 0\r\n foreach ($field in $item.fields) {\r\n $frow = \"\" | Select-Object Type,Name,Value\r\n switch ($field.type) {\r\n \"0\" { $frow.Type = \"Text\" }\r\n \"1\" { $frow.Type = \"Protected\" }\r\n \"2\" { $frow.Type = \"YesNo\" }\r\n }\r\n if ($null -eq $field.name) {\r\n $frow.Name = \"UnnamedField$($fieldIndex)\"\r\n $fieldIndex++\r\n } else {\r\n $frow.Name = $field.name\r\n }\r\n $frow.Value = $field.value\r\n\r\n $itemFields += $frow\r\n }\r\n } else {\r\n $itemFields += @{ Type = \"Header\"; Name = \"See notes for details\"; Value = \"\"; }\r\n }\r\n $row.CustomProperties = $itemFields\r\n $items += $row\r\n }\r\n }\r\n }\r\n\r\n return $items\r\n}\r\n\r\n# Get Vault status\r\n$status = (Invoke-Command -ScriptBloc { & \"$($Bitwarden.exec_path)\" status }) | ConvertFrom-Json\r\n\r\nif ($null -ne $status) {\r\n switch ($status.status) {\r\n \"unauthenticated\" {\r\n if ($null -eq $status.serverUrl -or $status.serverUrl -ne $Bitwarden.serverUrl) {\r\n # Vault not configured, configure server\r\n [void](Invoke-Command -ScriptBlock { & \"$($Bitwarden.exec_path)\" config server \"$($Bitwarden.serverUrl)\" })\r\n }\r\n\r\n # Prepare Vault login using API key\r\n $env:BW_CLIENTID = $Bitwarden.clientId\r\n $env:BW_CLIENTSECRET = $Bitwarden.clientSecret\r\n $env:BW_PASSWORD = $Bitwarden.password\r\n [void](Invoke-Command -ScriptBlock { & \"$($Bitwarden.exec_path)\" login --apikey})\r\n\r\n # Unlock Vault using password\r\n $Bitwarden.session = Invoke-Command -ScriptBlock { & \"$($Bitwarden.exec_path)\" unlock --passwordenv BW_PASSWORD --raw }\r\n\r\n if ($null -eq $Bitwarden.session -or $Bitwarden.session -eq \"\") {\r\n Write-Error -Message \"Unable to authenticate and unlock your vault. Please check your API credentials and master password in Custom Properties.\" -ErrorAction Stop\r\n }\r\n\r\n # Clear env variables\r\n Remove-Item -Path Env:\\BW_*\r\n }\r\n \"locked\" {\r\n # Vault is locked, unlock it with password\r\n $env:BW_PASSWORD = $Bitwarden.password\r\n $Bitwarden.session = Invoke-Command -ScriptBlock { & \"$($Bitwarden.exec_path)\" unlock --passwordenv BW_PASSWORD --raw }\r\n\r\n if ($null -eq $Bitwarden.session -or $Bitwarden.session -eq \"\") {\r\n Write-Error -Message \"Unable to unlock your vault. Please check your master password in Custom Properties.\" -ErrorAction Stop\r\n }\r\n\r\n # Clear env variables\r\n Remove-Item -Path Env:\\BW_*\r\n }\r\n }\r\n} else {\r\n Write-Error -Message \"Unable to get Vault status, check Server URL in Custom Properties or your connectivity.\" -ErrorAction Stop\r\n}\r\n\r\nif ($null -ne $Bitwarden.session) {\r\n # Sync Vault to latest version from server\r\n [void](Invoke-Command -ScriptBlock { & \"$($Bitwarden.exec_path)\" sync --session \"$($Bitwarden.session)\"})\r\n\r\n # Get and parse Personal Vault folders\r\n $tmpFolders = (Invoke-Command -ScriptBloc { & \"$($Bitwarden.exec_path)\" list folders --session \"$($Bitwarden.session)\"}) | ConvertFrom-Json\r\n\r\n foreach ($folder in $tmpFolders) {\r\n if ($null -ne $folder.id) {\r\n $tF = @{ Type = \"Folder\"; ID = $folder.id; Name = $folder.name; Objects = [array]@(Get-VaultItems -folderId $folder.id); }\r\n if ($tF.Objects.Count -ne 0) { $final.Objects[0].Objects += $tF; $tF = $null }\r\n } else {\r\n # Add default folder\r\n $tF = @{ Type = \"Folder\"; ID = \"nofolder\"; Name = \"No folder\"; Objects = [array]@(Get-VaultItems -folderId null); }\r\n if ($tF.Objects.Count -ne 0) { $final.Objects[0].Objects += $tF; $tF = $null }\r\n }\r\n }\r\n\r\n # Get and parse Organisations and Collections\r\n $organizations = (Invoke-Command -ScriptBloc { & \"$($Bitwarden.exec_path)\" list organizations --session \"$($Bitwarden.session)\"}) | ConvertFrom-Json\r\n\r\n foreach ($org in $organizations) {\r\n # Get collections for the organization\r\n $collections = (Invoke-Command -ScriptBloc { & \"$($Bitwarden.exec_path)\" list collections --organizationid $org.id --session \"$($Bitwarden.session)\"}) | ConvertFrom-Json\r\n $tOrgCollections = [array]@()\r\n foreach ($coll in $collections) {\r\n $tF = @{ Type = \"Folder\"; ID = $coll.id; Name = $coll.name; IconName = \"Flat/Software/Tree\"; Objects = [array]@(Get-VaultItems -collectionId $coll.id); }\r\n if ($tF.Objects.Count -ne 0) { $tOrgCollections += $tF; $tF = $null }\r\n }\r\n if ($tOrgCollections.Count -gt 0) {\r\n # Create organization folder\r\n $final.Objects += @{ Type = \"Folder\"; ID = $org.id; Name = $org.name; IconName = \"Flat/Money/Bank\"; Objects = $tOrgCollections; }\r\n }\r\n }\r\n}\r\n\r\n# Adapt JSON output for PowerShell version\r\nif ($PSVersionTable.PSVersion -ge '6.2') {\r\n #ConvertTo-Json -InputObject $final -Depth 100 -EscapeHandling EscapeHtml |Out-file \".\\bitwarden_output.json\" -Force\r\n ConvertTo-Json -InputObject $final -Depth 100 -EscapeHandling EscapeHtml\r\n} else {\r\n #ConvertTo-Json -InputObject $final -Depth 100 |Out-file \".\\bitwarden_output.json\" -Force\r\n ConvertTo-Json -InputObject $final -Depth 100\r\n}\r\n", "Type": "DynamicFolder", "Name": "Bitwarden (PowerShell)", "Description": "This Dynamic Folder sample allows you to import credentials from Bitwarden using Powershell.",