diff --git a/Samples/VaultExtension/Modules/CodeLocalVault/CodeLocalVault.psd1 b/Samples/VaultExtension/Modules/CodeLocalVault/CodeLocalVault.psd1 new file mode 100644 index 0000000..14d0471 --- /dev/null +++ b/Samples/VaultExtension/Modules/CodeLocalVault/CodeLocalVault.psd1 @@ -0,0 +1,4 @@ +@{ + ModuleVersion = '1.0' + RequiredAssemblies = @('CodeLocalVault') +} diff --git a/Samples/VaultExtension/Modules/CodeLocalVault/README.md b/Samples/VaultExtension/Modules/CodeLocalVault/README.md new file mode 100644 index 0000000..a6e6cce --- /dev/null +++ b/Samples/VaultExtension/Modules/CodeLocalVault/README.md @@ -0,0 +1,21 @@ +# CodeLocalVault + +This is a simple binary module vault extension example. +It contains a PowerShell module manifest (CodeLocalVault.psd1) and a C# source file and dotNet csproj file needed to build the binary assembly. +This module uses Export-CliXml/Import-CliXml cmdlets to store and retrieve secret objects to a file. + +This is **not** a secure storage, and is only intended as a vault extension example. + +You can experiment with this vault extension example by first building a CodeLocalVault.dll binary assembly, and then copying this module to your local machine under a file path that PowerShell uses to discover modules (e.g., within a $env:PSModulePath path). + +You can then register this as a Secrets Management local vault extension with the following command: + +```powershell +Register-SecretsVault -Name CodeLocalVault -ModuleName CodeLocalVault + +Get-SecretsVault CodeLocalVault + +Name ModuleName ImplementingType +---- ---------- ---------------- +CodeLocalVault CodeLocalVault CodeLocalVault.ImplementingClass +``` diff --git a/Samples/VaultExtension/Modules/CodeLocalVault/build/CodeLocalVault.cs b/Samples/VaultExtension/Modules/CodeLocalVault/build/CodeLocalVault.cs new file mode 100644 index 0000000..657e579 --- /dev/null +++ b/Samples/VaultExtension/Modules/CodeLocalVault/build/CodeLocalVault.cs @@ -0,0 +1,270 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.PowerShell.SecretsManagement; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Management.Automation; +using System.Management.Automation.Runspaces; +using System.Security; + +namespace CodeLocalVault +{ + #region ImplementingClass + + public class ImplementingClass : SecretsManagementExtension + { + #region Constructors + + private ImplementingClass() : base(string.Empty) + { } + + public ImplementingClass(string vaultName) : base(vaultName) + { } + + #endregion + + #region Abstract implementations + + public override bool SetSecret( + string name, + object secret, + IReadOnlyDictionary parameters, + out Exception error) + { + var results = PowerShellInvoker.InvokeCommand( + command: "Set-Secret", + args: new object[] { name, secret }, + dataStreams: out PSDataStreams dataStreams); + + if (dataStreams.Error.Count > 0) + { + error = dataStreams.Error[0].Exception; + return false; + } + + error = null; + return true; + } + + public override object GetSecret( + string name, + IReadOnlyDictionary parameters, + out Exception error) + { + var result = PowerShellInvoker.InvokeCommand( + command: "Get-Secret", + args: new object[] { name }, + dataStreams: out PSDataStreams dataStreams); + + error = dataStreams.Error.Count > 0 ? dataStreams.Error[0].Exception : null; + + return result.Count > 0 ? result[0] : null; + } + + public override bool RemoveSecret( + string name, + IReadOnlyDictionary parameters, + out Exception error) + { + PowerShellInvoker.InvokeCommand( + command: "Remove-Secret", + args: new object[] { name }, + dataStreams: out PSDataStreams dataStreams); + + if (dataStreams.Error.Count > 0) + { + error = dataStreams.Error[0].Exception; + return false; + } + + error = null; + return true; + } + + public override KeyValuePair[] GetSecretInfo( + string filter, + IReadOnlyDictionary parameters, + out Exception error) + { + var results = PowerShellInvoker.InvokeCommand( + command: "Enumerate-Secrets", + args: new object[] { filter }, + dataStreams: out PSDataStreams dataStreams); + + error = dataStreams.Error.Count > 0 ? dataStreams.Error[0].Exception : null; + + var list = new List>(results.Count); + foreach (dynamic result in results) + { + string typeName; + var resultValue = (result.Value is PSObject) ? ((PSObject)result.Value).BaseObject : result.Value; + switch (resultValue) + { + case byte[] blob: + typeName = nameof(SupportedTypes.ByteArray); + break; + + case string str: + typeName = nameof(SupportedTypes.String); + break; + + case SecureString sstr: + typeName = nameof(SupportedTypes.SecureString); + break; + + case PSCredential cred: + typeName = nameof(SupportedTypes.PSCredential); + break; + + case Hashtable ht: + typeName = nameof(SupportedTypes.Hashtable); + break; + + default: + typeName = nameof(SupportedTypes.Unknown); + break; + } + + list.Add( + new KeyValuePair( + key: result.Name, + value: typeName)); + } + + return list.ToArray(); + } + + #endregion + } + + #endregion + + #region PowerShellInvoker + + internal static class PowerShellInvoker + { + #region Members + + private const string FunctionsDefScript = @" + function Get-Path + { + $path = Join-Path $env:TEMP 'TVECode' + if (! (Test-Path -Path $path)) + { + [System.IO.Directory]::CreateDirectory($path) + } + + return $path + } + + function Get-Secret + { + param( + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string] $Name + ) + + if ([WildcardPattern]::ContainsWildcardCharacters($Name)) + { + throw ""The Name parameter cannot contain any wild card characters."" + } + + $filePath = Join-Path -Path (Get-Path) -ChildPath ""${Name}.xml"" + + if (! (Test-Path -Path $filePath)) + { + return + } + + Import-CliXml -Path $filePath + } + + function Enumerate-Secrets + { + param( + [string] $Name + ) + + if ([string]::IsNullOrEmpty($Name)) { $Name = '*' } + + $files = dir(Join-Path -Path (Get-Path) -ChildPath ""${Name}.xml"") 2>$null + + foreach ($file in $files) + { + $secretName = [System.IO.Path]::GetFileNameWithoutExtension((Split-Path $file -Leaf)) + $secret = Import-Clixml -Path $file.FullName + Write-Output([pscustomobject] @{ + Name = $secretName + Value = $secret + }) + } + } + + function Set-Secret + { + param( + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string] $Name, + + [Parameter(Mandatory=$true)] + [ValidateNotNull()] + [object] $Secret + ) + + $filePath = Join-Path -Path (Get-Path) ""${Name}.xml"" + if (Test-Path -Path $filePath) + { + Write-Error ""Secret name, $Name, is already used in this vault."" + return + } + + $Secret | Export-Clixml -Path $filePath + } + + function Remove-Secret + { + param( + [string] $Name + ) + + $filePath = Join-Path -Path (Get-Path) ""${Name}.xml"" + if (! (Test-Path -Path $filePath)) + { + Write-Error ""The secret $Name does not exist"" + return + } + + Remove-Item -Path $filePath + } + "; + + #endregion + + #region Methods + + public static Collection InvokeCommand( + string command, + object[] args, + out PSDataStreams dataStreams) + { + using (var powerShell = System.Management.Automation.PowerShell.Create()) + { + powerShell.AddScript(FunctionsDefScript).Invoke(); + powerShell.Commands.Clear(); + + var results = powerShell.AddCommand(command).AddParameters(args).Invoke(); + dataStreams = powerShell.Streams; + return results; + } + } + } + + #endregion + + #endregion +} diff --git a/Samples/VaultExtension/Modules/CodeLocalVault/build/CodeLocalVault.csproj b/Samples/VaultExtension/Modules/CodeLocalVault/build/CodeLocalVault.csproj new file mode 100644 index 0000000..2a80600 --- /dev/null +++ b/Samples/VaultExtension/Modules/CodeLocalVault/build/CodeLocalVault.csproj @@ -0,0 +1,16 @@ + + + + netcoreapp2.1 + + + + + .\Modules\Microsoft.PowerShell.SecretsManagement\Microsoft.PowerShell.SecretsManagement.dll + + + C:\Program Files\PowerShell\6\System.Management.Automation.dll + + + + diff --git a/Samples/VaultExtension/Modules/ScriptLocalVault/ImplementingModule/ImplementingModule.psd1 b/Samples/VaultExtension/Modules/ScriptLocalVault/ImplementingModule/ImplementingModule.psd1 new file mode 100644 index 0000000..43cb434 --- /dev/null +++ b/Samples/VaultExtension/Modules/ScriptLocalVault/ImplementingModule/ImplementingModule.psd1 @@ -0,0 +1,5 @@ +@{ + ModuleVersion = '1.0' + RootModule = '.\ImplementingModule.psm1' + FunctionsToExport = @('Set-Secret','Get-Secret','Remove-Secret','Get-SecretInfo') +} diff --git a/Samples/VaultExtension/Modules/ScriptLocalVault/ImplementingModule/ImplementingModule.psm1 b/Samples/VaultExtension/Modules/ScriptLocalVault/ImplementingModule/ImplementingModule.psm1 new file mode 100644 index 0000000..270ec34 --- /dev/null +++ b/Samples/VaultExtension/Modules/ScriptLocalVault/ImplementingModule/ImplementingModule.psm1 @@ -0,0 +1,100 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +function Get-Path +{ + $path = Join-Path $env:TEMP 'TVEScript' + if (! (Test-Path -Path $path)) + { + [System.IO.Directory]::CreateDirectory($path) + } + + return $path +} + +function Get-Secret +{ + param ( + [string] $Name, + [hashtable] $AdditionalParameters + ) + + if ([WildcardPattern]::ContainsWildcardCharacters($Name)) + { + throw "The Name parameter cannot contain wild card characters." + } + + $filePath = Join-Path -Path (Get-Path) -ChildPath "${Name}.xml" + if (! (Test-Path -Path $filePath)) + { + return + } + + return Import-Clixml -Path $filePath +} + +function Set-Secret +{ + param ( + [string] $Name, + [object] $Secret, + [hashtable] $AdditionalParameters + ) + + $filePath = Join-Path -Path (Get-Path) -ChildPath "${Name}.xml" + if (Test-Path -Path $filePath) + { + Write-Error "Secret name, $Name, is already used in this vault." + return $false + } + + $Secret | Export-Clixml -Path $filePath + return $true +} + +function Remove-Secret +{ + param ( + [string] $Name, + [hashtable] $AdditionalParameters + ) + + $filePath = Join-Path -Path (Get-Path) -ChildPath "${Name}.xml" + if (! (Test-Path -Path $filePath)) + { + Write-Error "The secret, $Name, does not exist." + return $false + } + + Remove-Item -Path $filePath + return $true +} + +function Get-SecretInfo +{ + param( + [string] $Filter, + [hashtable] $AdditionalParameters + ) + + if ([string]::IsNullOrEmpty($Filter)) { $Filter = "*" } + + $files = Get-ChildItem -Path (Join-Path -Path (Get-Path) -ChildPath "${Filter}.xml") 2>$null + + foreach ($file in $files) + { + $secretName = [System.IO.Path]::GetFileNameWithoutExtension((Split-Path -Path $file -Leaf)) + $secret = Import-Clixml -Path $file.FullName + $typeName = if ($secret -is [byte[]]) { "ByteArray" } + elseif ($secret -is [string]) { "String" } + elseif ($secret -is [securestring]) { "SecureString" } + elseif ($secret -is [PSCredential]) { "PSCredential" } + elseif ($secret -is [hashtable]) { "Hashtable" } + else { "Unknown" } + + Write-Output ([pscustomobject] @{ + Name = $secretName + Value = $typeName + }) + } +} diff --git a/Samples/VaultExtension/Modules/ScriptLocalVault/README.md b/Samples/VaultExtension/Modules/ScriptLocalVault/README.md new file mode 100644 index 0000000..f0ca852 --- /dev/null +++ b/Samples/VaultExtension/Modules/ScriptLocalVault/README.md @@ -0,0 +1,18 @@ +# ScriptLocalVault + +This is a simple script module vault extension example which uses PowerShell's Export-CliXml/Import-CliXml cmdlets to store and retrieve secret objects to a file. + +This is **not** a secure storage, and is only intended as a vault extension example. + +You can experiment with this vault extension example by copying this module to your local machine under a file path that PowerShell uses to discover modules (e.g., within a $env:PSModulePath path). +You can then register this as a Secrets Management local vault extension with the following command: + +```powershell +Register-SecretsVault -Name ScriptLocalVault -ModuleName ScriptLocalVault + +Get-SecretsVault ScriptLocalVault + +Name ModuleName ImplementingType +---- ---------- ---------------- +ScriptLocalVault ScriptLocalVault +``` diff --git a/Samples/VaultExtension/Modules/ScriptLocalVault/ScriptLocalVault.psd1 b/Samples/VaultExtension/Modules/ScriptLocalVault/ScriptLocalVault.psd1 new file mode 100644 index 0000000..a9e6eaf --- /dev/null +++ b/Samples/VaultExtension/Modules/ScriptLocalVault/ScriptLocalVault.psd1 @@ -0,0 +1,3 @@ +@{ + ModuleVersion = '1.0' +} diff --git a/Samples/VaultExtension/README.md b/Samples/VaultExtension/README.md new file mode 100644 index 0000000..2a69cc7 --- /dev/null +++ b/Samples/VaultExtension/README.md @@ -0,0 +1,11 @@ +# VaultExtension + +Microsoft.PowerShell.SecretsManagement module vault extension examples. + +This folder contains example extension vaults for the Secrets Management module. +These examples are meant only to illustrate how to create vault extensions, and are not intended to be used for secure storage of secrets. + +Vault extensions are PowerShell modules that implement required functions. +The modules can be script based or a binary assembly from compiled C# code. + +See the Docs folder for more information about the Secrets Management module and vault extensions.