From 7ad9cfb60cc4d7328022a9e5520633b11a868325 Mon Sep 17 00:00:00 2001 From: Nicolas GRIMLER Date: Mon, 7 Aug 2023 18:51:26 +0200 Subject: [PATCH 1/2] Add Dynamic Folder Bitwarden PowerShell script New version using Windows PowerShell 5.1 or PowerShell Core 6.x/7.x Improvements: * Use of API Key for login (no 2FA constraint) and master password for unlock * Add basic folder hierarchy * Add support for organizations and collections --- .../Bitwarden/Bitwarden (PowerShell).rdfe | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 Dynamic Folder/Bitwarden/Bitwarden (PowerShell).rdfe diff --git a/Dynamic Folder/Bitwarden/Bitwarden (PowerShell).rdfe b/Dynamic Folder/Bitwarden/Bitwarden (PowerShell).rdfe new file mode 100644 index 0000000..3439d98 --- /dev/null +++ b/Dynamic Folder/Bitwarden/Bitwarden (PowerShell).rdfe @@ -0,0 +1,46 @@ +{ + "Name": "Dynamic Folder Export", + "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", + "Type": "DynamicFolder", + "Name": "Bitwarden (PowerShell)", + "Description": "This Dynamic Folder sample allows you to import credentials from Bitwarden using Powershell.", + "CustomProperties": [ + { + "Name": "Bitwarden CLI Configuration", + "Type": "Header", + "Value": "" + }, + { + "Name": "BitWarden CLI Executable", + "Type": "Text", + "Value": "\\bw.exe" + }, + { + "Name": "BitWarden Server URL", + "Type": "URL", + "Value": "https://vault.bitwarden.com" + }, + { + "Name": "API Client ID", + "Type": "Protected", + "Value": "user." + }, + { + "Name": "API Client Secret", + "Type": "Protected", + "Value": "" + }, + { + "Name": "Account Password", + "Type": "Protected", + "Value": "" + } + ], + "ScriptInterpreter": "powershell", + "DynamicCredentialScriptInterpreter": "json" + } + ] +} \ No newline at end of file From 3f53cd86a6806e282d8acd93a4611fa347b7ce4f Mon Sep 17 00:00:00 2001 From: Nicolas GRIMLER Date: Mon, 7 Aug 2023 20:26:59 +0200 Subject: [PATCH 2/2] Added some error handling * Check CLI utility path validity * Added error messages for login/unlock * Fixed a left-over dump of the plaintext json result (enable for debugging only!) --- Dynamic Folder/Bitwarden/Bitwarden (PowerShell).rdfe | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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\t
  • Specify the full, absolute path to the Bitwarden CLI tool in the "Custom Properties" section.
  • Specify your server URL if on-premise instance, or offical Bitwarden URL
  • Specify your ClientID & ClientSecret for the API
  • Specify you master password to unlock the vault
\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.",