├── .github └── workflows │ └── powershell.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── SecretManagement.1Password.Extension ├── SecretManagement.1Password.Extension.psd1 └── SecretManagement.1Password.Extension.psm1 ├── SecretManagement.1Password.psd1 ├── build.ps1 └── tests ├── Get-Secret.Tests.ps1 ├── Get-SecretInfo.Tests.ps1 ├── Remove-Secret.Tests.ps1 ├── Set-Secret.Tests.ps1 └── Test-SecretVault.Tests.ps1 /.github/workflows/powershell.yml: -------------------------------------------------------------------------------- 1 | name: PowerShell 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | paths-ignore: 7 | - 'docs/**' 8 | - 'Changelog.md' 9 | - 'README.md' 10 | pull_request: 11 | branches: [ "main" ] 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | build: 18 | permissions: 19 | contents: read # for actions/checkout to fetch code 20 | security-events: write # for github/codeql-action/upload-sarif to upload SARIF results 21 | actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status 22 | name: Build 23 | runs-on: ubuntu-latest 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | with: 28 | fetch-depth: 0 # avoid shallow clone so nbgv can do its work. 29 | 30 | - name: Run PSScriptAnalyzer 31 | uses: microsoft/psscriptanalyzer-action@6b2948b1944407914a58661c49941824d149734f 32 | with: 33 | # Check https://github.com/microsoft/action-psscriptanalyzer for more info about the options. 34 | # The below set up runs PSScriptAnalyzer to your entire repository and runs some basic security rules. 35 | path: .\SecretManagement.1Password.Extension 36 | recurse: true 37 | # Include your own basic security rules. Removing this option will run all the rules 38 | includeRule: '"PSAvoidGlobalAliases", "PSAvoidUsingConvertToSecureStringWithPlainText"' 39 | output: results.sarif 40 | 41 | # Upload the SARIF file generated in the previous step 42 | - name: Upload SARIF results file 43 | uses: github/codeql-action/upload-sarif@v2 44 | with: 45 | sarif_file: results.sarif 46 | 47 | - uses: dotnet/nbgv@1801854259a50d987aaa03b99b28cebf49faa779 48 | id: nbgv 49 | 50 | - name: Build 51 | shell: pwsh 52 | run: ./build.ps1 -Package 53 | 54 | - name: Store build output 55 | uses: actions/upload-artifact@v4 56 | with: 57 | name: build 58 | path: | 59 | publish 60 | retention-days: 1 61 | 62 | test7: 63 | permissions: 64 | contents: read # for actions/checkout to fetch code 65 | actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status 66 | name: Test PowerShell 7 67 | needs: Build 68 | runs-on: ubuntu-latest 69 | container: 70 | image: mcr.microsoft.com/powershell:${{ matrix.pwshv }}-ubuntu-22.04 71 | strategy: 72 | matrix: 73 | pwshv: ['7.3','7.4'] 74 | 75 | steps: 76 | - uses: actions/checkout@v4 77 | 78 | - name: Download build output 79 | uses: actions/download-artifact@v4 80 | with: 81 | name: build 82 | path: publish 83 | 84 | - name: Install Utils 85 | shell: pwsh 86 | run: | 87 | apt-get update 88 | apt-get install curl jq -y 89 | 90 | - uses: testspace-com/setup-testspace@v1 91 | with: 92 | domain: ${{github.repository_owner}} 93 | 94 | - name: Test 95 | shell: pwsh 96 | run: ./build.ps1 -Test 97 | 98 | - name: Publish Results to Testspace 99 | run: testspace "[v${{ matrix.pwshv }}]testResults.xml" 100 | 101 | if: always() 102 | 103 | test5: 104 | permissions: 105 | contents: read # for actions/checkout to fetch code 106 | actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status 107 | name: Test PowerShell 5 108 | needs: Build 109 | runs-on: windows-latest 110 | steps: 111 | - uses: actions/checkout@v4 112 | with: 113 | fetch-depth: 0 114 | 115 | - name: Download build output 116 | uses: actions/download-artifact@v4 117 | with: 118 | name: build 119 | path: publish 120 | 121 | - uses: testspace-com/setup-testspace@v1 122 | with: 123 | domain: ${{github.repository_owner}} 124 | 125 | - name: Test 126 | shell: powershell 127 | run: ./build.ps1 -Test 128 | 129 | - name: Publish Results to Testspace 130 | run: testspace "[v5.1]testResults.xml" 131 | if: always() 132 | 133 | publish: 134 | permissions: 135 | contents: read # for actions/checkout to fetch code 136 | actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status 137 | name: Publish 138 | needs: [test7, test5] 139 | runs-on: ubuntu-latest 140 | container: 141 | image: mcr.microsoft.com/dotnet/sdk:8.0 142 | if: github.ref == 'refs/heads/main' 143 | steps: 144 | - uses: actions/checkout@v4 145 | 146 | - name: Download build output 147 | uses: actions/download-artifact@v4 148 | with: 149 | name: build 150 | path: publish 151 | 152 | - name: Publish 153 | shell: pwsh 154 | run: ./build.ps1 -Publish 155 | env: 156 | PSPublishApiKey: ${{ secrets.NUGETAPIKEY }} 157 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/* 2 | !.vscode/tasks.json 3 | !.vscode/launch.json 4 | !.vscode/extensions.json 5 | *.code-workspace 6 | 7 | # Local History for Visual Studio Code 8 | .history/ 9 | 10 | release -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright © 2020 Chris Hunt 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SecretManagement extension for 1Password 2 | 3 | This powershell module is a 4 | [SecretManagement](https://github.com/PowerShell/SecretManagement) 5 | extension for 6 | [1Password](https://1password.com/). 7 | It leverages the [`1password-cli`](https://support.1password.com/command-line/) 8 | to interact with 1Password. 9 | 10 | The SecretManagment.1Password module requires that the 1Password CLI application is installed and configured to access 1Password. 11 | 12 | ## Prerequisites 13 | 14 | * [PowerShell](https://github.com/PowerShell/PowerShell) 15 | * The [`1password-cli`](https://support.1password.com/command-line/) and accessible from Path 16 | * Enable access to 1Password through one of the following methods: 17 | * Activate the [1Password app integration](https://developer.1password.com/docs/cli/app-integration/) 18 | * [Add a new 1Password account to 1Password CLI manually](https://developer.1password.com/docs/cli/reference/management-commands/account#account-add) with your account password and Secret Key. 19 | ```pwsh 20 | op account add --address my.1password.com --email user@example.org 21 | ``` 22 | * The [SecretManagement PowerShell](https://github.com/PowerShell/SecretManagement) module 23 | 24 | You can get the `SecretManagement` module from the PowerShell Gallery: 25 | 26 | Using PowerShellGet v2: 27 | 28 | ```pwsh 29 | Install-Module Microsoft.PowerShell.SecretManagement 30 | ``` 31 | 32 | Using PowerShellGet v3: 33 | 34 | ```pwsh 35 | Install-PSResource Microsoft.PowerShell.SecretManagement -Prerelease 36 | ``` 37 | ## Installation 38 | 39 | This module can be installed from the PowerShell Gallery: 40 | 41 | Using PowerShellGet v2: 42 | 43 | ```pwsh 44 | Install-Module SecretManagement.1Password 45 | ``` 46 | 47 | Using PowerShellGet v3: 48 | 49 | ```pwsh 50 | Install-PSResource SecretManagement.1Password 51 | ``` 52 | 53 | ## Registration 54 | 55 | Once the SecretManagement.1Password module is installed, a SecretManagement vault must be registered as follows: 56 | 57 | ```pwsh 58 | Register-SecretVault -Name '1Password: MyVaultName' ` 59 | -ModuleName 'SecretManagement.1Password' ` 60 | -VaultParameters @{AccountName='myaccount.1password.com'; OPVault = 'MyVaultName'} 61 | ``` 62 | Next are the detials provided in the registration: 63 | * **Name**: Name of the SecretManagement vault. This will be the name to use when managing secrets from the SecretManagement powershell cmdlets. 64 | * **ModuleName**: Name of the PowerShell extension module that will be interacting with the underlyging secrets source. In this case, as the source will be "1Password" the extension module name must be "SecretManagement.1Password". 65 | * **VaultParameters**: Optional. Details required by the extension module to access the source secrets. See the section [Vault parameters](#Vault-parameters) for details specific for the SecretManagement.1Password extension module (used to access 1Password). 66 | 67 | **Note**: The name given to the SecretManagement vault (provided with the `Name` parameter) doesn't need to match the name of an existing vault in 1Password. Considering that the SecretManagement module supports multiple sources, it may be useful to prefix each of its vaults with a word that allows to know the source. For instance, in the case of 1Password vaults, the SecretManagement vaults can be named as "1Password: VaultName". 68 | 69 | It is recommended to regiser one SecretManagement vault for each 1Password vault that need to be accessed. 70 | 71 | 72 | ### Vault parameters 73 | 74 | The module also has the following vault parameter that must be provided at registration. 75 | 76 | ```pwsh 77 | $vaultParameters = @{ 78 | AccountName = 'myaccount.1password.com' 79 | OPVault = 'MyVaultName' 80 | } 81 | ``` 82 | 83 | #### AccountName 84 | 85 | Optional. Specifies what 1Password account to connect to when accessing secrets. It is common to have a corporate and a personal account. This parameter allows to select one of your accounts. If this parameter is not provided, then the default 1Password account will be used. 86 | 87 | The 1Password account name can be found in the URL used to access 1Password as follows: 88 | 89 | ``` 90 | https://myaccountname.1password.com/ 91 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 92 | ``` 93 | Corporate accounts are typically accessed through a URL like `https://myaccountname.1password.com/`. In this example, the account name is `myaccountname.1password.com`. 94 | 95 | Personal accounts typically have `my.1password.com` as account name. 96 | 97 | #### OPVault 98 | 99 | Name or Id of the 1Password vault associated with the SecretManagement vault. 100 | 101 | If this parameter is missing then the 1Password CLI will search on all vaults in the target account. 102 | 103 | > [!WARNING] 104 | > Not linking the SecretManagement vault with a unique 1Password vault may cause issues because there may be more than one secret, stored in different 1Password vaults, sharing the same name. In that case, retrieval and updates uperations will have issues. 105 | 106 | ## Dependencies 107 | 108 | This module extension has been developed and tested with the following dependencies' version: 109 | * **PowerShell**: 5.1 110 | * **Microsoft.PowerShell.SecretManagement**: 1.1.2 111 | * **1Password CLI**: 2.30.0 112 | 113 | ## Known issues 114 | 115 | ### Development issue: Reimporting the extension module (or the parent SecretManagement module) doesn't refresh changes made in the extension module after the later has been previusly loaded 116 | 117 | > [!NOTE] 118 | > This issue affects only to developers of this module extension. Regular users are not affected. 119 | 120 | The SecretManagement.1Password module is an extension for the main module Microsoft.PowerShell.SecretManagement. 121 | 122 | The need to nest extension modules comes due to the fact that all extension modules for Microsoft.PowerShell.SecretManagement, contain the same public function names which would be overwritten if more than one extension module (vault type) were loaded on the same session. 123 | 124 | While developing there is the need to make changes to the functions of the extension module and then run them to see the effect. This can be done by loading the extension module (*.psm1) as a main module and then calling directly its functions. However, this approach presents limitations: 125 | 126 | - It doesn't allow to see the parameters being passed by the main module (Microsoft.PowerShell.SecretManagement). 127 | - It doesn't allow to see the transformations made by the parent module before the output is being finally returned to the calling code. 128 | 129 | To see all effects of running the extension module as a nested extension of its parent module (Microsoft.PowerShell.SecretManagement) it is needed to import the parent module and then call one of the cmdlets associated with a vault registered with Microsoft.PowerShell.SecretManagement. Let's see an example 130 | 131 | ```pwsh 132 | # Import the main module. 133 | Import-Module Microsoft.PowerShell.SecretManagement 134 | # Make sure the target vault is registered with its associated extension module. 135 | Register-SecretVault -Name "MyVaultName" ` 136 | -ModuleName 'PathToModule\SecretManagement.1Password\SecretManagement.1Password.psd1' ` 137 | -VaultParameters @{OPVault = 'Employee'} ` 138 | -AllowClobber 139 | # Call an extension cmdlet through the main module. 140 | Get-Secret -Vault "MyVaultName" -Name "MySecretName" 141 | ``` 142 | Note that the extension module is referenced with the path to the main *.psd1 file of the extension module. This path is specific for each development environment. 143 | 144 | The above code works well if the extension module is not changed. However, if changes are made, then re-importing the main module will not refresh the extension module in the PowerShell cache with the new changes. Even the following code, that uses the -Force parameter and explicitly unload both, the main and the extension modules, will not solve the issue: 145 | 146 | ```pwsh 147 | Get-Module SecretManagement.1Password | Remove-Module -Force; 148 | Get-Module Microsoft.PowerShell.SecretManagement | Remove-Module -Force; 149 | Import-Module Microsoft.PowerShell.SecretManagement -Force; 150 | Import-Module 'PathToModule\SecretManagement.1Password\SecretManagement.1Password.psd1' -Force; 151 | Get-SecretVault "MyVaultName" | Unregister-SecretVault 152 | Register-SecretVault -Name "MyVaultName" ` 153 | -ModuleName 'SecretManagement.1Password' ` 154 | -VaultParameters @{OPVault = 'Employee'} ` 155 | -AllowClobber 156 | ``` 157 | This is a [known issue](https://github.com/PowerShell/PowerShell/issues/2505#issuecomment-263105859) discussed internally by the PowerShell team who reached to the conclusion that [it is by "design"](https://github.com/PowerShell/PowerShell/issues/2505#issuecomment-902325128). 158 | 159 | Not being able to reload nested modules during development time also affects Pester tests which require the console session to be re-started every time a change is made in the functions of the extension (nested) module. The easiest way to restart the console, in VSCode, to avoid restarting the development environment, is as follows: 160 | 1. Click on the commands box, on the top of the main window 161 | 1. Select "Show and Run Commands >" 162 | 1. Run "PowerShell: Restart Session" 163 | 164 | #### References 165 | 166 | - [Reloading module does not reload submodules](https://github.com/PowerShell/PowerShell/issues/2505#issuecomment-263105859) 167 | - [Conclusion from the PowerShell team about the issue](https://github.com/PowerShell/PowerShell/issues/2505#issuecomment-902325128) -------------------------------------------------------------------------------- /SecretManagement.1Password.Extension/SecretManagement.1Password.Extension.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | ModuleVersion = '2.0.0.1' 3 | RootModule = 'SecretManagement.1Password.Extension.psm1' 4 | FunctionsToExport = @('Get-Secret','Get-SecretInfo','Test-SecretVault','Set-Secret','Remove-Secret') 5 | } -------------------------------------------------------------------------------- /SecretManagement.1Password.Extension/SecretManagement.1Password.Extension.psm1: -------------------------------------------------------------------------------- 1 | using namespace Microsoft.PowerShell.SecretManagement 2 | 3 | function Invoke-OpCommand{ 4 | <# 5 | .SYNOPSIS 6 | Calls the 1Password CLI console application and returns an object with three properties: 7 | StdOut: Text, excluding errors, returned by the application 8 | StdErr: Error text returned by the application, if any. 9 | ExitCode: Exit code of the command. ExitCode=0 means "Success". 10 | 11 | .DESCRIPTION 12 | Calling the op.exe application directly from PowerShell (with the prefix "&") doesn't allow to 13 | capture the text outputted in case of an error. This function solves the issue and suppress the 14 | need to redirecting the error text to "$null", to prevent its display to the user. 15 | 16 | .PARAMETER ArgumentList 17 | Argument list to be passed to the 1Password CLI console application. 18 | #> 19 | param( 20 | [Parameter( 21 | Mandatory=$true, 22 | Position=0, 23 | HelpMessage="Argument list to be passed to the 1Password CLI console application.")] 24 | [String[]]$ArgumentList 25 | ) 26 | 27 | $pinfo = [System.Diagnostics.ProcessStartInfo]::new(); 28 | $pinfo.FileName = "op.exe"; 29 | $pinfo.RedirectStandardError = $true; 30 | $pinfo.RedirectStandardOutput = $true; 31 | $pinfo.UseShellExecute = $false; 32 | $pinfo.Arguments = ($ArgumentList -join " "); 33 | $p = New-Object System.Diagnostics.Process; 34 | $p.StartInfo = $pinfo; 35 | $p.Start() | Out-Null; 36 | $stdout = $p.StandardOutput.ReadToEnd(); 37 | $stderr = $p.StandardError.ReadToEnd(); 38 | $p.WaitForExit(); 39 | return [PSCustomObject]@{ 40 | StdOut = $stdout; 41 | StdErr = $stderr; 42 | ExitCode = $p.ExitCode; 43 | } 44 | } 45 | 46 | function Test-SecretVault { 47 | [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")] 48 | [CmdletBinding()] 49 | param ( 50 | [Parameter(ValueFromPipelineByPropertyName, Mandatory)] 51 | [string]$VaultName, 52 | 53 | [Parameter(ValueFromPipelineByPropertyName)] 54 | [hashtable]$AdditionalParameters 55 | ) 56 | 57 | if (-not $VaultName) { 58 | Write-Error 'The name SecretManagement vault must be provided.' 59 | return $false 60 | } 61 | 62 | Write-Verbose "Validating the SecretManagement Vault '$($VaultName)'..." 63 | 64 | $secretVault = Get-SecretVault -Name $VaultName -ErrorAction SilentlyContinue 65 | if ($null -eq $secretVault){ 66 | Write-Error "The SecretManagement vault '$($VaultName)' is not registered." 67 | return $false 68 | } 69 | if ($null -eq $AdditionalParameters){ 70 | $VaultParameters = $secretVault.VaultParameters 71 | }else{ 72 | $VaultParameters = $AdditionalParameters 73 | } 74 | 75 | Write-Verbose "Validating the 1Password Vault parameters> AccountName: '$($VaultParameters.AccountName)'; OPVault: '$($VaultParameters.OPVault)'" 76 | 77 | if (-not $VaultParameters.AccountName) { Write-Warning 'The 1Password account (AccountName) is missing in the SecretManagement vault parameters.' } 78 | if (-not $VaultParameters.OPVault) { Write-Warning 'The 1Password vault name (OPVault) is missing in the SecretManagement vault parameters.' } 79 | 80 | Write-Verbose "Trying to read the 1Password vaults ..." 81 | $commandArgs = [System.Collections.ArrayList]::new(); 82 | $commandArgs.AddRange(@('vault', 'list')); 83 | if ($VaultParameters.AccountName) { 84 | $commandArgs.AddRange(@('--account', "$($VaultParameters.AccountName)")); 85 | } 86 | $commandArgs.AddRange(@('--format', 'json')); 87 | $result = Invoke-OpCommand $commandArgs; 88 | if ($result.ExitCode -ne 0){ 89 | #Error on execution 90 | Write-Error "An arror occurred while accessing 1Password: $($result.StdErr)"; 91 | return $false; 92 | }else{ 93 | Write-Verbose "1Password vaults successfully read." 94 | } 95 | $vaults = $result.StdOut | ConvertFrom-Json; 96 | if (-not $vaults) { 97 | Write-Error "No vaults were found in 1Password." 98 | return false; 99 | } 100 | if ($VaultParameters.OPVault) { 101 | $targetVault = $vaults.Where({ $_.name -eq $VaultParameters.OPVault -or $_.id -eq $VaultParameters.OPVault }) 102 | 103 | if ($targetVault){ 104 | Write-Verbose "1Password vault '$($VaultParameters.OPVault)' successfully found." 105 | return $true 106 | }else{ 107 | Write-Error "The vault '$($VaultParameters.OPVault)' was not found in 1Password." 108 | return $false 109 | } 110 | }else{ 111 | Write-Verbose "1Password contains '$($vaults.Count)' vaults." 112 | return ($vaults.Count -gt 0) 113 | } 114 | } 115 | 116 | function Get-SecretInfo { 117 | [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")] 118 | [CmdletBinding()] 119 | param ( 120 | [Parameter(ValueFromPipelineByPropertyName, Mandatory)] 121 | [string]$VaultName, 122 | [Parameter()] 123 | [string]$Filter, 124 | [Parameter()] 125 | [hashtable] $AdditionalParameters 126 | ) 127 | 128 | Write-Verbose "'Get-SecretInfo' invoked ..." 129 | 130 | if ($null -ne $AdditionalParameters){ 131 | $VaultParameters = $AdditionalParameters 132 | }else{ 133 | if ($null -eq $VaultName){$VaultName = ""} 134 | $secretVault = Get-SecretVault -Name $VaultName -ErrorAction SilentlyContinue 135 | if ($null -eq $secretVault){ 136 | Write-Error "The SecretManagement vault '$($VaultName)' is not registered." 137 | return $null 138 | } 139 | $VaultParameters = $secretVault.VaultParameters 140 | } 141 | 142 | $commandArgs = [System.Collections.ArrayList]::new(); 143 | $commandArgs.AddRange(@('item', 'list')); 144 | if ($VaultParameters.AccountName) { 145 | $commandArgs.AddRange(@('--account', """$($VaultParameters.AccountName)""")); 146 | } 147 | if ($VaultParameters.OPVault) { 148 | $commandArgs.AddRange(@('--vault', """$($VaultParameters.OPVault)""")); 149 | } 150 | $commandArgs.AddRange(@('--categories', '"LOGIN,PASSWORD"', '--format', 'json')); 151 | $result = Invoke-OpCommand $commandArgs; 152 | if ($result.ExitCode -eq 0){ 153 | $items = $result.StdOut -replace 'b5UserUUID', 'B5UserUUID' | ConvertFrom-Json; 154 | 155 | if (-not [string]::IsNullOrEmpty($Name)){ 156 | $items = $items | Where-Object { $_.title -eq $Name }; 157 | }else{ 158 | if ([string]::IsNullOrEmpty($Filter)){ 159 | $Filter = "*" 160 | } 161 | $items = $items | Where-Object { $_.title -like $Filter }; 162 | } 163 | }else{ 164 | $items = $null; 165 | } 166 | 167 | $keyList = [System.Collections.Generic.Dictionary[[string],[SecretInformation]]]::new(); 168 | 169 | foreach ($item in $items) { 170 | if ( $keyList.ContainsKey(($item.title).ToLower()) ) { 171 | Write-Verbose "Get-SecretInfo: An item with the same key has already been added. Key: [$($item.title)]" 172 | } 173 | else { 174 | $type = switch ($item.category) { 175 | 'LOGIN' { [SecretType]::PSCredential } 176 | 'PASSWORD' { [SecretType]::SecureString } 177 | Default { [SecretType]::Unknown } 178 | } 179 | 180 | Write-Verbose $item.title 181 | 182 | # The vault name to be returned within the SecretInformation object must be the name of the SecretManagement 183 | # vault because the SecretInformation object can be passed to the Get-Secret cmdlet to query secrets, which 184 | # will require to have the name of the SecretManagement vault. 185 | # See: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.secretmanagement/get-secret?view=ps-modules#-inputobject 186 | $keyList.Add( ` 187 | $(($item.title).ToLower()), ` 188 | [SecretInformation]::new($item.title, $type, $($VaultName)) ` 189 | ); 190 | } 191 | } 192 | 193 | return [SecretInformation[]]$keyList.Values 194 | } 195 | 196 | function Get-Secret { 197 | [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")] 198 | [CmdletBinding()] 199 | param ( 200 | [Parameter()] 201 | [string]$Name, 202 | [Parameter()] 203 | [string]$Filter, 204 | [Parameter()] 205 | [string]$VaultName, 206 | [Parameter()] 207 | [switch]$AsPlainText, 208 | [Parameter()] 209 | [hashtable] $AdditionalParameters 210 | ) 211 | 212 | Write-Verbose "'Get-Secret' invoked ..." 213 | 214 | if ($null -ne $AdditionalParameters){ 215 | $VaultParameters = $AdditionalParameters 216 | }else{ 217 | if ($null -eq $VaultName){$VaultName = ""} 218 | $secretVault = Get-SecretVault -Name $VaultName -ErrorAction SilentlyContinue 219 | if ($null -eq $secretVault){ 220 | Write-Error "The SecretManagement vault '$($VaultName)' is not registered." 221 | return $null 222 | } 223 | $VaultParameters = $secretVault.VaultParameters 224 | } 225 | 226 | $commandArgs = [System.Collections.ArrayList]::new(); 227 | $commandArgs.AddRange(@('item', 'get', """$($Name)""")); 228 | if ($VaultParameters.AccountName) { 229 | $commandArgs.AddRange(@('--account', """$($VaultParameters.AccountName)""")); 230 | } 231 | if ($VaultParameters.OPVault) { 232 | $commandArgs.AddRange(@('--vault', """$($VaultParameters.OPVault)""")); 233 | } 234 | $commandArgs.AddRange(@('--format', 'json')); 235 | $result = Invoke-OpCommand $commandArgs; 236 | if ($result.ExitCode -ne 0){ 237 | Write-Verbose $result.StdErr; 238 | return $null; # Not found 239 | } 240 | $item = $result.StdOut | ConvertFrom-Json; 241 | 242 | # Check existence of Time-based One Time Password (TOTP) 243 | $totp = -1 244 | if ($item.fields.type -contains "OTP") { 245 | $totp = $item.fields.Where({ $_.type -eq 'OTP' }) | Select-Object -ExpandProperty totp 246 | } 247 | 248 | $password = $item.fields.Where({ $_.id -eq 'password' }) 249 | $username = $item.fields.Where({ $_.id -eq 'username' }) 250 | 251 | if ( -not [string]::IsNullOrEmpty($password.value) -and -not $AsPlainText) { 252 | [securestring]$secureStringPassword = ConvertTo-SecureString $password.value -AsPlainText -Force 253 | } 254 | 255 | $output = $null 256 | 257 | if ([string]::IsNullOrEmpty($password.value) -and -not [string]::IsNullOrEmpty($username.value)) { 258 | $output = @{UserName = $username.value } 259 | } elseif ([string]::IsNullOrEmpty($username.value)) { 260 | if ($AsPlainText) { 261 | if($totp -gt -1){ 262 | $output = @{Password = $username.value; totp = $totp } 263 | } else { 264 | $output = $username.value 265 | } 266 | } else { 267 | if($totp -gt -1){ 268 | $output = @{Password = $secureStringPassword; totp = $totp } 269 | } else { 270 | $output = $secureStringPassword 271 | } 272 | } 273 | } else { 274 | if ($AsPlainText) { 275 | if($totp -gt -1){ 276 | $output = @{UserName = $username.value; Password = $username.value; totp = $totp } 277 | } else { 278 | $output = $username.value 279 | } 280 | } else { 281 | if($totp -gt -1){ 282 | $output = @{ 283 | Credentials = [PSCredential]::new( 284 | $username.value, 285 | $secureStringPassword 286 | ); 287 | totp = $totp 288 | } 289 | } else { 290 | $output = [PSCredential]::new( 291 | $username.value, 292 | $secureStringPassword 293 | ) 294 | } 295 | } 296 | 297 | } 298 | 299 | return $output 300 | 301 | } 302 | 303 | function Set-Secret { 304 | [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")] 305 | [CmdletBinding()] 306 | param ( 307 | [Parameter()] 308 | [string]$Name, 309 | [Parameter()] 310 | [object]$Secret, 311 | [Parameter()] 312 | [string]$VaultName, 313 | [Parameter()] 314 | [hashtable] $AdditionalParameters 315 | ) 316 | 317 | Write-Verbose "'Set-Secret' invoked ..." 318 | 319 | if ($null -ne $AdditionalParameters){ 320 | $VaultParameters = $AdditionalParameters 321 | }else{ 322 | if ($null -eq $VaultName){$VaultName = ""} 323 | $secretVault = Get-SecretVault -Name $VaultName -ErrorAction SilentlyContinue 324 | if ($null -eq $secretVault){ 325 | Write-Error "The SecretManagement vault '$($VaultName)' is not registered." 326 | return $null 327 | } 328 | $VaultParameters = $secretVault.VaultParameters 329 | } 330 | 331 | $commandArgs = [System.Collections.ArrayList]::new(); 332 | $commandArgs.AddRange(@('item', 'get', """$($Name)""")); 333 | if ($VaultParameters.AccountName) { 334 | $commandArgs.AddRange(@('--account', """$($VaultParameters.AccountName)""")); 335 | } 336 | if ($VaultParameters.OPVault) { 337 | $commandArgs.AddRange(@('--vault', """$($VaultParameters.OPVault)""")); 338 | } 339 | $commandArgs.AddRange(@('--format', 'json')); 340 | $result = Invoke-OpCommand $commandArgs; 341 | 342 | if ($result.ExitCode -ne 0){ 343 | if ($result.StdErr.Contains("More than one item matches")){ 344 | throw [Exception]::new($result.StdErr); 345 | return $null; 346 | } 347 | # Not found 348 | $verb = 'create'; 349 | }else{ 350 | # Found and there is only one 351 | $verb = 'edit'; 352 | } 353 | Write-Verbose $verb 354 | $commandArgs = [System.Collections.ArrayList]::new(); 355 | $commandArgs.AddRange(@('item', $verb)); 356 | if ($VaultParameters.AccountName) { 357 | $commandArgs.AddRange(@('--account', """$($VaultParameters.AccountName)""")); 358 | } 359 | if ($VaultParameters.OPVault) { 360 | $commandArgs.AddRange(@('--vault', """$($VaultParameters.OPVault)""")); 361 | } 362 | $commandArgs.AddRange(@('--format', 'json')); 363 | 364 | <# 365 | op item create --category=login --title='My Example Item' --vault='Test' ` 366 | --url https://www.acme.com/login ` 367 | --generate-password='letters,digits,symbols,32' ` 368 | username=jane@acme.com ` 369 | 'Test Field 1=my test secret' ` 370 | 'Test Section 1.Test Field2[text]=Jane Doe' ` 371 | 'Test Section 1.Test Field3[date]=1995-02-23' ` 372 | 'Test Section 2.Test Field4[text]=Testing 1Password CLI' 373 | #> 374 | 375 | Write-Verbose "Secret type [$($Secret.GetType().Name)]" 376 | switch ($Secret.GetType()) { 377 | { $_.Name -eq 'String' -or $_.IsValueType } { 378 | $category = "Password" 379 | Write-Verbose "Processing [string] as '$category'" 380 | 381 | if ('create' -eq $verb ) { 382 | Write-Verbose "Creating '$Name'" 383 | 384 | $commandArgs.Add("--category=$category") | Out-Null 385 | $commandArgs.Add("--title=""$Name""") | Out-Null 386 | $commandArgs.Add("password=""$Secret""") | Out-Null 387 | } 388 | else { 389 | Write-Verbose "Updating '$Name'" 390 | 391 | $commandArgs.Add("""$Name""") | Out-Null 392 | $commandArgs.Add("password=""$Secret""") | Out-Null 393 | } 394 | break 395 | } 396 | { $_.Name -eq 'securestring' } { 397 | $category = "Password" 398 | Write-Verbose "Processing [securestring] as '$category'" 399 | 400 | if ('create' -eq $verb ) { 401 | Write-Verbose "Creating ""$Name""" 402 | $commandArgs.Add("--category=$category") | Out-Null 403 | $commandArgs.Add("--title=""$Name""") | Out-Null 404 | $commandArgs.Add("password=""$([System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Secret)))""") | Out-Null 405 | } 406 | else { 407 | Write-Verbose "Updating '$Name'" 408 | $commandArgs.Add("""$Name""") | Out-Null 409 | $commandArgs.Add("password=""$([System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Secret)))""") | Out-Null 410 | } 411 | break 412 | } 413 | { $_.Name -eq 'PSCredential' } { 414 | $category = "Login" 415 | Write-Verbose "Processing [PSCredential] as $category" 416 | 417 | if ('create' -eq $verb ) { 418 | Write-Verbose "Creating '$Name'" 419 | 420 | $commandArgs.Add("--category=$category") | Out-Null 421 | $commandArgs.Add("--title=""$Name""") | Out-Null 422 | $commandArgs.Add("username=""$($Secret.UserName)""") | Out-Null 423 | $commandArgs.Add("password=""$($Secret.GetNetworkCredential().Password)""") | Out-Null 424 | } 425 | else { 426 | Write-Verbose "Updating '$Name'" 427 | $commandArgs.Add("""$Name""") | Out-Null 428 | $commandArgs.Add("username=""$($Secret.UserName)""") | Out-Null 429 | $commandArgs.Add("password=""$($Secret.GetNetworkCredential().Password)""") | Out-Null 430 | } 431 | break 432 | } 433 | Default {} 434 | } 435 | 436 | $sanitizedArgs = $commandArgs | ForEach-Object { 437 | if ($_ -like 'password=*') { 438 | 'password=*****' 439 | } else { 440 | $_ 441 | } 442 | } 443 | Write-Verbose ($sanitizedArgs -join ' ') 444 | 445 | $result = Invoke-OpCommand $commandArgs; 446 | #$result.StdOut; 447 | #$result.StdErr; 448 | return ($result.ExitCode -eq 0); 449 | } 450 | 451 | function Remove-Secret { 452 | [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")] 453 | [CmdletBinding()] 454 | param ( 455 | [Parameter()] 456 | [string]$Name, 457 | [Parameter()] 458 | [string]$VaultName, 459 | [Parameter()] 460 | [hashtable] $AdditionalParameters 461 | ) 462 | 463 | Write-Verbose "'Remove-Secret' invoked ..." 464 | 465 | if ($null -ne $AdditionalParameters){ 466 | $VaultParameters = $AdditionalParameters 467 | }else{ 468 | if ($null -eq $VaultName){$VaultName = ""} 469 | $secretVault = Get-SecretVault -Name $VaultName -ErrorAction SilentlyContinue 470 | if ($null -eq $secretVault){ 471 | Write-Error "The SecretManagement vault '$($VaultName)' is not registered." 472 | return $null 473 | } 474 | $VaultParameters = $secretVault.VaultParameters 475 | } 476 | 477 | $commandArgs = [System.Collections.ArrayList]::new(); 478 | $commandArgs.AddRange(@('item', 'delete', """$($Name)""")); 479 | if ($VaultParameters.AccountName) { 480 | $commandArgs.AddRange(@('--account', """$($VaultParameters.AccountName)""")); 481 | } 482 | if ($VaultParameters.OPVault) { 483 | $commandArgs.AddRange(@('--vault', """$($VaultParameters.OPVault)""")); 484 | } 485 | $commandArgs.Add("--archive") | Out-Null 486 | Write-Verbose ($commandArgs -join ' ') 487 | 488 | $result = Invoke-OpCommand $commandArgs; 489 | #$result.StdOut; 490 | #$result.StdErr; 491 | if ($result.ExitCode -ne 0){ 492 | Write-Error "An arror occurred while trying to delete the secret '$($Name)' in 1Password: $($result.StdErr)"; 493 | } 494 | return ($result.ExitCode -eq 0); 495 | } 496 | -------------------------------------------------------------------------------- /SecretManagement.1Password.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'SecretManagement.1Password' 3 | # 4 | # Generated by: chunt 5 | # 6 | # Generated on: 11/17/2020 7 | # 8 | 9 | @{ 10 | 11 | # Script module or binary module file associated with this manifest. 12 | # RootModule = '' 13 | 14 | # Version number of this module. 15 | ModuleVersion = '1.0.1' 16 | 17 | # Supported PSEditions 18 | # CompatiblePSEditions = @() 19 | 20 | # ID used to uniquely identify this module 21 | GUID = '4c9df238-a61e-4398-9d30-aaaa67273193' 22 | 23 | # Author of this module 24 | Author = 'Chris Hunt' 25 | 26 | # Company or vendor of this module 27 | 28 | # Copyright statement for this module 29 | Copyright = '(c) Chris Hunt. All rights reserved.' 30 | 31 | # Description of the functionality provided by this module 32 | Description = 'SecretManagement extension for 1Password' 33 | 34 | # Minimum version of the PowerShell engine required by this module 35 | PowerShellVersion = '5.1' 36 | 37 | # Name of the PowerShell host required by this module 38 | # PowerShellHostName = '' 39 | 40 | # Minimum version of the PowerShell host required by this module 41 | # PowerShellHostVersion = '' 42 | 43 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 44 | # DotNetFrameworkVersion = '' 45 | 46 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 47 | # ClrVersion = '' 48 | 49 | # Processor architecture (None, X86, Amd64) required by this module 50 | # ProcessorArchitecture = '' 51 | 52 | # Modules that must be imported into the global environment prior to importing this module 53 | # RequiredModules = @() 54 | 55 | # Assemblies that must be loaded prior to importing this module 56 | # RequiredAssemblies = @() 57 | 58 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 59 | # ScriptsToProcess = @() 60 | 61 | # Type files (.ps1xml) to be loaded when importing this module 62 | # TypesToProcess = @() 63 | 64 | # Format files (.ps1xml) to be loaded when importing this module 65 | # FormatsToProcess = @() 66 | 67 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 68 | NestedModules = './SecretManagement.1Password.Extension' 69 | 70 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 71 | FunctionsToExport = @() 72 | 73 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 74 | CmdletsToExport = @() 75 | 76 | # Variables to export from this module 77 | VariablesToExport = '*' 78 | 79 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 80 | AliasesToExport = @() 81 | 82 | # DSC resources to export from this module 83 | # DscResourcesToExport = @() 84 | 85 | # List of all modules packaged with this module 86 | # ModuleList = @() 87 | 88 | # List of all files packaged with this module 89 | # FileList = @() 90 | 91 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 92 | PrivateData = @{ 93 | 94 | PSData = @{ 95 | 96 | # Tags applied to this module. These help with module discovery in online galleries. 97 | Tags = 'SecretManagement', 'Secrets', '1Password', 'MacOS', 'Linux', 'Windows' 98 | 99 | # A URL to the license for this module. 100 | LicenseUri = 'https://raw.githubusercontent.com/cdhunt/SecretManagement.1Password/master/LICENSE.txt' 101 | 102 | # A URL to the main website for this project. 103 | ProjectUri = 'https://github.com/cdhunt/SecretManagement.1Password' 104 | 105 | # A URL to an icon representing this module. 106 | # IconUri = '' 107 | 108 | # ReleaseNotes of this module 109 | # ReleaseNotes = '' 110 | 111 | # Prerelease string of this module 112 | # Prerelease = 'rc1' 113 | 114 | # Flag to indicate whether the module requires explicit user acceptance for install/update/save 115 | # RequireLicenseAcceptance = $false 116 | 117 | # External dependent modules of this module 118 | # ExternalModuleDependencies = @() 119 | 120 | } # End of PSData hashtable 121 | 122 | } # End of PrivateData hashtable 123 | 124 | # HelpInfo URI of this module 125 | # HelpInfoURI = '' 126 | 127 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 128 | # DefaultCommandPrefix = '' 129 | 130 | } 131 | 132 | -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | [Parameter()] 4 | [switch] 5 | $Test, 6 | 7 | [Parameter()] 8 | [switch] 9 | $Package, 10 | 11 | [Parameter()] 12 | [switch] 13 | $Publish 14 | ) 15 | 16 | Push-Location $PSScriptRoot 17 | 18 | if ($Test) { 19 | Invoke-Pester tests 20 | } 21 | 22 | if ($Package) { 23 | $outDir = Join-Path 'release' 'SecretManagement.1Password' 24 | Remove-Item release -Recurse -Force -ErrorAction SilentlyContinue | Out-Null 25 | 26 | @( 27 | 'SecretManagement.1Password.Extension' 28 | 'SecretManagement.1Password.psd1' 29 | 'LICENSE.txt' 30 | 'README.md' 31 | ) | ForEach-Object { 32 | Copy-Item -Path $_ -Destination (Join-Path $outDir $_) -Force -Recurse 33 | } 34 | } 35 | 36 | if ($Publish) { 37 | Write-Host -ForegroundColor Green "Publishing module... here are the details:" 38 | $moduleData = Import-Module -Force ./release/SecretManagement.1Password -PassThru 39 | Write-Host "Version: $($moduleData.Version)" 40 | Write-Host "Prerelease: $($moduleData.PrivateData.PSData.Prerelease)" 41 | Write-Host -ForegroundColor Green "Here we go..." 42 | 43 | Publish-Module -Path ./release/SecretManagement.1Password -NuGetApiKey $env:PSGALLERYAPIKEY 44 | } 45 | 46 | Pop-Location -------------------------------------------------------------------------------- /tests/Get-Secret.Tests.ps1: -------------------------------------------------------------------------------- 1 | # This assumes 1Password is already registered and unlocked 2 | 3 | BeforeAll { 4 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] 5 | $testDetails = @{ 6 | Vault = '1Password: Employee' 7 | LoginName = 'TestLogin' + (Get-Random -Maximum 99999) 8 | PasswordName = 'TestPassword' + (Get-Random -Maximum 99999) 9 | TOTPName = 'TestTime-boundOneTimePassword' + (Get-Random -Maximum 99999) 10 | UserName = 'TestUserName' 11 | Password = 'TestPassword' 12 | VaultParameters = @{ 13 | OPVault = 'Employee' 14 | } 15 | } 16 | 17 | Get-Module SecretManagement.1Password | Remove-Module -Force 18 | Get-Module Microsoft.PowerShell.SecretManagement | Remove-Module -Force 19 | Register-SecretVault -ModuleName (Join-Path $PSScriptRoot '..\SecretManagement.1Password.psd1') -Name $testDetails.Vault -VaultParameters $testDetails.VaultParameters -AllowClobber 20 | } 21 | 22 | Describe 'It gets logins with vault specified' { 23 | BeforeAll { 24 | # Create the login, if it doesn't already exist. 25 | $item = & op item get "$($testDetails.LoginName)" --vault "$($testDetails.VaultParameters.OPVault)" 2>$null 26 | if ($null -eq $item) { 27 | & op item create --category=Login --title="$($testDetails.LoginName)" "username=$($testDetails.UserName)" "password=$($testDetails.Password)" 28 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] 29 | $createdLogin = $true 30 | } else { 31 | Write-Warning "An item called $($testDetails.LoginName) already exists" 32 | } 33 | } 34 | 35 | It 'Gets a login' { 36 | Get-Secret -Vault "$($testDetails.Vault)" -Name "$($testDetails.LoginName)" | Should -BeOfType PSCredential 37 | } 38 | 39 | It 'Gets the login username with vault specified' { 40 | $cred = Get-Secret -Vault "$($testDetails.Vault)" -Name "$($testDetails.LoginName)" 41 | $cred.UserName | Should -Be $testDetails.UserName 42 | } 43 | 44 | It 'Gets the login password with vault specified' { 45 | $cred = Get-Secret -Vault "$($testDetails.Vault)" -Name "$($testDetails.LoginName)" 46 | $([System.Runtime.InteropServices.Marshal]::PtrToStringAuto( ` 47 | [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($cred.Password))) | Should -Be $testDetails.Password 48 | } 49 | 50 | AfterAll { 51 | if ($createdLogin) {& op item delete "$($testDetails.LoginName)"} 52 | } 53 | } 54 | 55 | Describe 'It gets passwords with vault specified' { 56 | BeforeAll { 57 | # Create the password, if it doesn't already exist. 58 | $item = & op item get "$($testDetails.PasswordName)"--vault "$($testDetails.VaultParameters.OPVault)" 2>$null 59 | if ($null -eq $item) { 60 | & op item create --category=Password --title="$($testDetails.PasswordName)" "password=$($testDetails.Password)" 61 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] 62 | $createdPassword = $true 63 | } else { 64 | Write-Warning "An item called $($testDetails.PasswordName) already exists" 65 | } 66 | } 67 | 68 | It 'Gets a password' { 69 | Get-Secret -Vault "$($testDetails.Vault)" -Name $testDetails.PasswordName | Should -BeOfType SecureString 70 | } 71 | 72 | It 'Gets the password with vault specified' { 73 | Get-Secret -Vault "$($testDetails.Vault)" -Name $testDetails.PasswordName -AsPlainText | 74 | Should -Be $testDetails.Password 75 | } 76 | 77 | AfterAll { 78 | if ($createdPassword) {& op item delete "$($testDetails.PasswordName)"} 79 | } 80 | } 81 | 82 | Describe 'It gets one-time passwords with vault specified' { 83 | BeforeAll { 84 | # Relies on an item called TOTPTest with TOTP set up being present 85 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] 86 | $item = & op item get "$($testDetails.TOTPName)" --vault "$($testDetails.VaultParameters.OPVault)" 2>$null 87 | if ($null -eq $item) { 88 | & op item create --category=Password --title="$($testDetails.TOTPName)" --vault "$($testDetails.VaultParameters.OPVault)" --generate-password=20,letters,digits "TotpField[otp]=otpauth://totp/:?secret=&issuer=" 89 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] 90 | $createdPassword = $true 91 | #Write-Verbose "An item called $($testDetails.TOTPName) created for the tests" 92 | } else { 93 | Write-Warning "An item called $($testDetails.TOTPName) already exists" 94 | $createdPassword = $false; 95 | } 96 | } 97 | 98 | It 'Gets a TOTP' { 99 | $secret=Get-Secret -Vault "$($testDetails.Vault)" -Name "$($testDetails.TOTPName)" 100 | $members = switch ($secret) { 101 | { $_ -is [System.Collections.Hashtable] } { 102 | $secret.Keys; 103 | break; 104 | } 105 | { $_ -is [PSObject] } { 106 | Get-Member -InputObject $secret -MemberType Property | Select-Object -ExpandProperty Name 107 | break; 108 | } 109 | { $_ -is [PSCustomObject]} { 110 | Get-Member -InputObject $secret -MemberType NoteProperty | Select-Object -ExpandProperty Name; 111 | break; 112 | } 113 | default { 114 | [String[]]@() 115 | } 116 | } 117 | $members | Should -Contain 'totp' 118 | } 119 | 120 | It 'Gets the TOTP with vault specified' { 121 | $secret = Get-Secret -Vault "$($testDetails.Vault)" -Name "$($testDetails.TOTPName)" 122 | # Timing issues, test will be flaky 123 | $secret.totp | Should -BeGreaterThan -1 124 | } 125 | 126 | AfterAll { 127 | if ($createdPassword) {& op item delete "$($testDetails.TOTPName)"} 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /tests/Get-SecretInfo.Tests.ps1: -------------------------------------------------------------------------------- 1 | # This assumes 1Password is already registered and unlocked 2 | 3 | BeforeAll { 4 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] 5 | $testDetails = @{ 6 | Vault = '1Password: Employee' 7 | LoginName = 'TestLogin' + (Get-Random -Maximum 99999) 8 | PasswordName = 'TestPassword' + (Get-Random -Maximum 99999) 9 | UserName = 'TestUserName' 10 | Password = 'TestPassword' 11 | VaultParameters = @{ 12 | OPVault = 'Employee' 13 | } 14 | } 15 | 16 | Get-Module SecretManagement.1Password | Remove-Module -Force 17 | Get-Module Microsoft.PowerShell.SecretManagement | Remove-Module -Force 18 | Register-SecretVault -ModuleName (Join-Path $PSScriptRoot '..\SecretManagement.1Password.psd1') -Name $testDetails.Vault -VaultParameters $testDetails.VaultParameters -AllowClobber 19 | } 20 | 21 | Describe 'It gets items' { 22 | BeforeAll { 23 | # Create the login, if it doesn't already exist. 24 | $item = & op item get "$($testDetails.LoginName)" --vault "$($testDetails.VaultParameters.OPVault)" 2>$null 25 | if ($null -eq $item) { 26 | & op item create --category=Login --title="$($testDetails.LoginName)" "username=$($testDetails.UserName)" "password=$($testDetails.Password)" 27 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] 28 | $createdLogin = $true 29 | } else { 30 | Write-Warning "An item called $($testDetails.LoginName) already exists" 31 | $createdLogin = $false 32 | } 33 | } 34 | 35 | It 'returns all items' { 36 | $info = Get-SecretInfo -Vault "$($testDetails.Vault)" 37 | $info.Count | Should -BeGreaterOrEqual 1 38 | } 39 | 40 | it 'filters items' { 41 | $info = Get-SecretInfo -Vault "$($testDetails.Vault)" -Name "$($testDetails.LoginName)" 42 | $info | Should -HaveCount 1 43 | } 44 | 45 | AfterAll { 46 | if ($createdLogin) {& op item delete "$($testDetails.LoginName)"} 47 | } 48 | } 49 | 50 | Describe 'It gets login info with vault specified' { 51 | BeforeAll { 52 | # Create the login, if it doesn't already exist. 53 | $item = & op item get "$($testDetails.LoginName)" --vault "$($testDetails.VaultParameters.OPVault)" 2>$null 54 | if ($null -eq $item) { 55 | & op item create --category=Login --title="$($testDetails.LoginName)" "username=$($testDetails.UserName)" "password=$($testDetails.Password)" 56 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] 57 | $createdLogin = $true 58 | } else { 59 | Write-Warning "An item called $($testDetails.LoginName) already exists" 60 | $createdLogin = $false 61 | } 62 | } 63 | 64 | It 'returns logins as PSCredentials' { 65 | $info = Get-SecretInfo -Vault "$($testDetails.Vault)" -Name "$($testDetails.LoginName)" 66 | $info | Should -BeOfType [Microsoft.PowerShell.SecretManagement.SecretInformation] 67 | $info.Type | Should -Be PSCredential 68 | } 69 | 70 | AfterAll { 71 | if ($createdLogin) {& op item delete "$($testDetails.LoginName)"} 72 | } 73 | } 74 | 75 | Describe 'It gets password info with vault specified' { 76 | BeforeAll { 77 | # Create the password, if it doesn't already exist. 78 | $item = & op item get "$($testDetails.PasswordName)" --vault "$($testDetails.VaultParameters.OPVault)" 2>$null 79 | if ($null -eq $item) { 80 | & op item create --category=Password --title="$($testDetails.PasswordName)" "password=$($testDetails.Password)" 81 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] 82 | $createdPassword = $true 83 | } else { 84 | Write-Warning "An item called $($testDetails.PasswordName) already exists" 85 | $createdPassword = $false 86 | } 87 | } 88 | 89 | It 'returns passwords as SecureStrings' { 90 | $info = Get-SecretInfo -Vault "$($testDetails.Vault)" -Name $testDetails.PasswordName 91 | $info | Should -BeOfType [Microsoft.PowerShell.SecretManagement.SecretInformation] 92 | $info.Type | Should -Be SecureString 93 | } 94 | 95 | AfterAll { 96 | if ($createdPassword) {& op item delete "$($testDetails.PasswordName)"} 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/Remove-Secret.Tests.ps1: -------------------------------------------------------------------------------- 1 | # This assumes 1Password is already registered and unlocked 2 | 3 | BeforeAll { 4 | $testDetails = @{ 5 | Vault = '1Password: Employee' 6 | LoginName = 'TestLogin' + (Get-Random -Maximum 99999) 7 | UserName = 'TestUserName' 8 | Password = 'TestPassword' 9 | VaultParameters = @{ 10 | OPVault = 'Employee' 11 | } 12 | } 13 | 14 | Get-Module SecretManagement.1Password | Remove-Module -Force 15 | Get-Module Microsoft.PowerShell.SecretManagement | Remove-Module -Force 16 | Register-SecretVault -ModuleName (Join-Path $PSScriptRoot '..\SecretManagement.1Password.psd1') -Name $testDetails.Vault -VaultParameters $testDetails.VaultParameters -AllowClobber 17 | 18 | } 19 | 20 | Describe 'It removes items' { 21 | BeforeEach { 22 | # Create the login, if it doesn't already exist. 23 | $item = & op item get "$($testDetails.LoginName)" --vault "$($testDetails.VaultParameters.OPVault)" 2>$null 24 | if ($null -eq $item) { 25 | & op item create --category=Login --title="$($testDetails.LoginName)" --vault "$($testDetails.VaultParameters.OPVault)" "username=$($testDetails.UserName)" "password=$($testDetails.Password)" 26 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] 27 | $createdLogin = $true 28 | } else { 29 | Write-Warning "An item called $($testDetails.LoginName) already exists. Remove-Item test will be skipped." 30 | $createdLogin = $false 31 | } 32 | } 33 | 34 | It 'It removes an item with vault specified' { 35 | # Skip the test if we did not create the login item 36 | # Use -ForEach to get the same $testDetails.LoginName value as in BeforeDiscovery 37 | Remove-Secret -Vault "$($testDetails.Vault)" -Name "$($testDetails.LoginName)" 38 | # Confirm the item no longer exists 39 | & op item get "$($testDetails.LoginName)" --vault "$($testDetails.VaultParameters.OPVault)" 2>$null | Should -Not -Contain "isn't an item in the ""$($testDetails.VaultParameters.OPVault)"" vault" 40 | } 41 | 42 | AfterEach { 43 | if ($createdLogin) { 44 | $item = & op item get "$($testDetails.LoginName)" --vault "$($testDetails.VaultParameters.OPVault)" 2>$null 45 | if ($null -ne $item) { 46 | & op item delete "$($testDetails.LoginName)" --vault "$($testDetails.VaultParameters.OPVault)" 47 | } 48 | 49 | 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/Set-Secret.Tests.ps1: -------------------------------------------------------------------------------- 1 | # This assumes 1Password is already registered and unlocked 2 | 3 | BeforeAll { 4 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] 5 | $testDetails = @{ 6 | Vault = '1Password: Employee' 7 | LoginName = 'TestLogin' + (Get-Random -Maximum 99999) 8 | UserName = 'TestUserName' 9 | Password = 'TestPassword' 10 | VaultParameters = @{ 11 | OPVault = 'Employee' 12 | } 13 | } 14 | 15 | Get-Module SecretManagement.1Password | Remove-Module -Force 16 | Get-Module Microsoft.PowerShell.SecretManagement | Remove-Module -Force 17 | Register-SecretVault -ModuleName (Join-Path $PSScriptRoot '..\SecretManagement.1Password.psd1') -Name $testDetails.Vault -VaultParameters $testDetails.VaultParameters -AllowClobber 18 | } 19 | 20 | Describe 'It updates items that already exist' { 21 | BeforeAll { 22 | # Create the login, if it doesn't already exist. 23 | $item = & op item get "$($testDetails.LoginName)" --vault "$($testDetails.VaultParameters.OPVault)" 2>$null 24 | if ($null -eq $item) { 25 | & op item create --category=Login --title="$($testDetails.LoginName)" --vault "$($testDetails.VaultParameters.OPVault)" "username=$($testDetails.UserName)" "password=$($testDetails.Password)" 26 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] 27 | $createdLogin = $true 28 | } else { 29 | Write-Warning "An item called $($testDetails.LoginName) already exists" 30 | $createdLogin = $false 31 | } 32 | } 33 | 34 | It 'Sets the password from an int value type with vault specified' { 35 | $testvalue = 123456 36 | Set-Secret -Vault "$($testDetails.Vault)" -Name "$($testDetails.LoginName)" -Secret $testvalue 37 | & op item get "$($testDetails.LoginName)" --fields password --vault "$($testDetails.VaultParameters.OPVault)" --reveal | Should -Be $testvalue 38 | } 39 | 40 | It 'Sets the password from a char value type with vault specified' { 41 | $testvalue = [char]'a' 42 | Set-Secret -Vault "$($testDetails.Vault)" -Name "$($testDetails.LoginName)" -Secret $testvalue 43 | & op item get "$($testDetails.LoginName)" --fields password --vault "$($testDetails.VaultParameters.OPVault)" --reveal | Should -Be $testvalue 44 | } 45 | 46 | It 'Sets the password from a string with vault specified' { 47 | $testvalue = 'String Password!' 48 | Set-Secret -Vault "$($testDetails.Vault)" -Name "$($testDetails.LoginName)" -Secret "$($testvalue)" 49 | & op item get "$($testDetails.LoginName)" --fields password --vault "$($testDetails.VaultParameters.OPVault)" --reveal | Should -Be $testvalue 50 | } 51 | 52 | It 'Sets the password from a SecureString with vault specified' { 53 | $testvalue = 'SecureString Password!' 54 | Set-Secret -Vault "$($testDetails.Vault)" -Name "$($testDetails.LoginName)" -Secret ($testvalue | ConvertTo-SecureString -AsPlainText -Force) 55 | & op item get "$($testDetails.LoginName)" --fields password --vault "$($testDetails.VaultParameters.OPVault)" --reveal | Should -Be $testvalue 56 | } 57 | 58 | It 'Sets the password from a PSCredential with vault specified' { 59 | $testvalue = 'PSCredential Password!' 60 | $testusername = 'PSCredential Username' 61 | $cred = [pscredential]::new($testusername, ($testvalue | ConvertTo-SecureString -AsPlainText -Force)) 62 | Set-Secret -Vault "$($testDetails.Vault)" -Name "$($testDetails.LoginName)" -Secret $cred 63 | & op item get "$($testDetails.LoginName)" --fields password --vault "$($testDetails.VaultParameters.OPVault)" --reveal | Should -Be $testvalue 64 | & op item get "$($testDetails.LoginName)" --fields username --vault "$($testDetails.VaultParameters.OPVault)" | Should -Be $testusername 65 | } 66 | 67 | AfterAll { 68 | if ($createdLogin) { 69 | & op item delete "$($testDetails.LoginName)" --vault "$($testDetails.VaultParameters.OPVault)" 70 | } 71 | } 72 | } 73 | 74 | Describe 'It creates items' { 75 | BeforeEach { 76 | $item = & op item get "$($testDetails.LoginName)" --vault "$($testDetails.VaultParameters.OPVault)" 2>$null 77 | if ($null -ne $item) { 78 | & op item delete "$($testDetails.LoginName)" --vault "$($testDetails.VaultParameters.OPVault)" 79 | Write-Verbose "Item '$($testDetails.LoginName)' detected and deleted before the test." 80 | } 81 | } 82 | 83 | It 'Sets the password from an int value type with vault specified' { 84 | $testvalue = 123456 85 | Set-Secret -Vault "$($testDetails.Vault)" -Name "$($testDetails.LoginName)" -Secret $testvalue 86 | & op item get "$($testDetails.LoginName)" --fields password --vault "$($testDetails.VaultParameters.OPVault)" --reveal | Should -Be $testvalue 87 | } 88 | 89 | It 'Sets the password from a char value type with vault specified' { 90 | $testvalue = [char]'a' 91 | Set-Secret -Vault "$($testDetails.Vault)" -Name "$($testDetails.LoginName)" -Secret $testvalue 92 | & op item get "$($testDetails.LoginName)" --fields password --vault "$($testDetails.VaultParameters.OPVault)" --reveal | Should -Be $testvalue 93 | } 94 | 95 | It 'Sets the password from a string with vault specified' { 96 | $testvalue = 'String Password!' 97 | Set-Secret -Vault "$($testDetails.Vault)" -Name "$($testDetails.LoginName)" -Secret $testvalue 98 | & op item get "$($testDetails.LoginName)" --fields password --vault "$($testDetails.VaultParameters.OPVault)" --reveal | Should -Be $testvalue 99 | } 100 | 101 | It 'Sets the password from a SecureString with vault specified' { 102 | $testvalue = 'SecureString Password!' 103 | Set-Secret -Vault "$($testDetails.Vault)" -Name "$($testDetails.LoginName)" -Secret ($testvalue | ConvertTo-SecureString -AsPlainText -Force) 104 | & op item get "$($testDetails.LoginName)" --fields password --vault "$($testDetails.VaultParameters.OPVault)" --reveal | Should -Be $testvalue 105 | } 106 | 107 | It 'Sets the username and password from a PSCredential with vault specified' { 108 | $testvalue = 'PSCredential Password!' 109 | $testusername = 'PSCredential Username' 110 | $cred = [pscredential]::new($testusername, ($testvalue | ConvertTo-SecureString -AsPlainText -Force)) 111 | Set-Secret -Vault "$($testDetails.Vault)" -Name "$($testDetails.LoginName)" -Secret $cred 112 | & op item get "$($testDetails.LoginName)" --fields password --vault "$($testDetails.VaultParameters.OPVault)" --reveal | Should -Be $testvalue 113 | & op item get "$($testDetails.LoginName)" --fields username --vault "$($testDetails.VaultParameters.OPVault)" | Should -Be $testusername 114 | } 115 | 116 | AfterEach { 117 | $item = & op item get "$($testDetails.LoginName)" --vault "$($testDetails.VaultParameters.OPVault)" 2>$null 118 | if ($null -ne $item) { 119 | & op item delete "$($testDetails.LoginName)" --vault "$($testDetails.VaultParameters.OPVault)" 120 | Write-Verbose "Item '$($testDetails.LoginName)' detected and deleted after the test." 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /tests/Test-SecretVault.Tests.ps1: -------------------------------------------------------------------------------- 1 | # This assumes 1Password is already registered and unlocked 2 | 3 | BeforeDiscovery { 4 | $testDetails = @{ 5 | Vault = '1Password: TestVault' + (Get-Random -Maximum 99999) 6 | SecretStoreVault = 'SecretManagement.1Password.Tests' 7 | VaultParameters = @{ 8 | OPVault = 'Employee' 9 | } 10 | } 11 | 12 | if ([System.String]::IsNullOrEmpty($env:OP_TEST_ACCOUNT)){ 13 | Write-Warning ("Some tests require the name of a 1Password account which is stablished through " + ` 14 | "the environment variable `$env:OP_TEST_ACCOUNT. If you want all tests to be completed, " + ` 15 | "please, set such environment variable. The following command can help:" + ` 16 | "`r`n`r`n`t`$env:OP_TEST_ACCOUNT = ""mytestaccount.1password.com""`r`n"); 17 | 18 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] 19 | $testAccountNotAvailable = $true; 20 | }else{ 21 | $testDetails.VaultParameters.AccountName = "$($env:OP_TEST_ACCOUNT)"; 22 | $testAccountNotAvailable = $false; 23 | } 24 | 25 | } 26 | 27 | Describe 'Unregistered SecretManagement vault for 1Password' -ForEach @{testDetails=$testDetails;} { 28 | BeforeAll { 29 | $secretVault = Get-SecretVault -Name "$($testDetails.Vault)" -ErrorAction SilentlyContinue 30 | if ($null -ne $secretVault) { 31 | Unregister-SecretVault "$($testDetails.Vault)"; 32 | Write-Warning "The SecretManagement vault '$($testDetails.Vault)' was unexpectedly detected before the test. It has been removed."; 33 | } 34 | } 35 | BeforeEach{ 36 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] 37 | $er = $null; 38 | Test-SecretVault -Name "$($testDetails.Vault)" -ErrorVariable er -ErrorAction SilentlyContinue; 39 | } 40 | 41 | It 'Write an error' { 42 | $er.Count | Should -BeGreaterThan 0; 43 | } 44 | 45 | It 'Meaningful error message' { 46 | $er.Exception.Message | Should -BeLike "* does not exist in registry*" 47 | } 48 | 49 | } 50 | 51 | Describe 'Missing all vault paremeters' -ForEach @{testDetails=$testDetails;}{ 52 | BeforeAll { 53 | # Register the vault without VaultParameters 54 | Register-SecretVault -ModuleName 'SecretManagement.1Password.psd1' -Name "$($testDetails.Vault)" -AllowClobber 55 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] 56 | $testValue = Test-SecretVault -Name "$($testDetails.Vault)" -WarningVariable wa; 57 | } 58 | 59 | It 'Writes one warnings for each missing optional parameter.' { 60 | $wa.Count | Should -Be 2 61 | } 62 | 63 | It 'Meaningful warning for missing optional parameters' { 64 | $wa.Message | Should -BeLike "* is missing in the SecretManagement vault parameters*" 65 | #$wa.Message.Where({$_ -like "* is missing in the SecretManagement vault parameters*"}) 66 | } 67 | 68 | It 'Returns $True if it can read vaults from 1Password using default settings.' { 69 | try{ 70 | $vaults=$null; 71 | $vaults= (& op vault list --format json) | ConvertFrom-Json; 72 | }catch{} 73 | ($null -ne $vaults -and $vaults.Count -gt 0) | Should -Be $testValue 74 | 75 | } 76 | 77 | AfterAll { 78 | # Clearing tests 79 | Unregister-SecretVault -Name "$($testDetails.Vault)" 80 | } 81 | } 82 | 83 | Describe 'Only the ''OPVault'' parameter (name of the 1Password vault) is configured in the VaultParemeters' -ForEach @{testDetails=$testDetails;} { 84 | BeforeAll { 85 | # Register the vault without VaultParameters 86 | Register-SecretVault -ModuleName 'SecretManagement.1Password.psd1' -Name "$($testDetails.Vault)" -VaultParameters @{OPVault="$($testDetails.VaultParameters.OPVault)"} -AllowClobber 87 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] 88 | $testValue = Test-SecretVault -Name "$($testDetails.Vault)" -WarningVariable wa; 89 | } 90 | 91 | It 'Writes one warnings for each missing optional parameter.' { 92 | $wa.Count | Should -Be 1 93 | } 94 | 95 | It 'Meaningful warning for missing optional parameters' { 96 | $wa.Message | Should -BeLike "* is missing in the SecretManagement vault parameters*" 97 | #$wa.Message.Where({$_ -like "* is missing in the SecretManagement vault parameters*"}) 98 | } 99 | 100 | It 'Returns $True if it can read vaults from the specified 1Password vault.' { 101 | try{ 102 | # The 1Password CLI API doesn't allow to filter by vault through flags. Thus, the filter must be applied locally. 103 | $vaults=$null; 104 | $vaults= (& op vault list --format json) | ConvertFrom-Json; 105 | $opVault = $testDetails.VaultParameters.OPVault; 106 | $vaults = $vaults | Where-Object{$_.name -eq $opVault -or $_.id -eq $opVault }; 107 | # $testDetails.VaultParameters.OPVault 108 | # $testDetails.VaultParameters.AccountName 109 | }catch{} 110 | ($null -ne $vaults) | Should -Be $testValue 111 | 112 | } 113 | 114 | AfterAll { 115 | # Clearing tests 116 | Unregister-SecretVault -Name "$($testDetails.Vault)" #-ErrorAction SilentlyContinue 117 | } 118 | } 119 | 120 | Describe 'Only the ''AccountName'' parameter (1Password account) is configured in the VaultParemeters' -Skip:($testAccountNotAvailable -eq $true) -ForEach @{testDetails=$testDetails;} { 121 | BeforeAll { 122 | # Register the vault without VaultParameters 123 | Register-SecretVault -ModuleName 'SecretManagement.1Password.psd1' -Name "$($testDetails.Vault)" -VaultParameters @{AccountName="$($testDetails.VaultParameters.AccountName)"} -AllowClobber 124 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] 125 | $testValue = Test-SecretVault -Name "$($testDetails.Vault)" -WarningVariable wa; 126 | } 127 | 128 | It 'Writes one warnings for each missing optional parameter.' { 129 | $wa.Count | Should -Be 1 130 | } 131 | 132 | It 'Meaningful warning for missing optional parameters' { 133 | $wa.Message | Should -BeLike "* is missing in the SecretManagement vault parameters*" 134 | #$wa.Message.Where({$_ -like "* is missing in the SecretManagement vault parameters*"}) 135 | } 136 | 137 | It 'Returns $True if it can read vaults from the specified 1Password vault.' { 138 | try{ 139 | # The 1Password CLI API doesn't allow to filter by vault through flags. Thus, the filter must be applied locally. 140 | $vaults=$null; 141 | $vaults= (& op vault list --account "$($testDetails.VaultParameters.AccountName)" --format json) | ConvertFrom-Json; 142 | }catch{} 143 | ($null -ne $vaults -and $vaults.Count -gt 0) | Should -Be $testValue 144 | 145 | } 146 | 147 | AfterAll { 148 | # Clearing tests 149 | Unregister-SecretVault -Name "$($testDetails.Vault)" 150 | } 151 | } 152 | 153 | --------------------------------------------------------------------------------