Secrecy.psm1
|
function Add-DynamicParam { <# .SYNOPSIS Adds a dynamic parameter to a script, within a DynamicParam block. .DESCRIPTION Adding dynamic parameters is a complex process, this attempts to simplify that. .INPUTS System.Object[] a list of possible values for this parameter to validate against. .FUNCTIONALITY PowerShell .EXAMPLE DynamicParam { Add-DynamicParam Path string -Mandatory; $DynamicParams } Process { Import-Variables $PSBoundParameters; ... } #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseProcessBlockForPipelineCommand','', Justification='This script uses $input within an End block.')] [CmdletBinding()][OutputType([void])] Param( # The name of the parameter. [Parameter(Position=0,Mandatory=$true)][string] $Name, # The data type of the parameter. [Parameter(Position=1)][type] $Type, # The position of the parameter when not specifying the parameter names. [int] $Position = -2147483648, # The name of the set of parameters this parameter belongs to. [string[]] $ParameterSetName = '__AllParameterSets', # Alternate names for the parameter. [string[]] $Alias, <# The valid number of values for a parameter that accepts a collection. A range can be specified with a list of two integers. #> [Alias('Count')][ValidateCount(1,2)][int[]] $ValidateCount, # Valid root drive(s) for parameters that accept paths. [string[]] $ValidateDrive, <# The valid length for a string parameter. A range can be specified with a list of two integers. #> [Alias('Length')][ValidateCount(1,2)][int[]] $ValidateLength, # The valid regular expression pattern to match for a string parameter. [Alias('Match','Pattern')][string] $ValidatePattern, # The valid range of values for a numeric parameter. [Alias('Range')][ValidateCount(2,2)][int[]] $ValidateRange, <# A script block to validate a parameter's value. Any true result will validate the value, any false result will reject it. #> [ScriptBlock] $ValidateScript, <# A set of valid values for the parameter. This will enable tab-completion. #> [Parameter(ValueFromPipeline=$true)][Alias('Values')][object[]] $ValidateSet, # Requires parameter to be non-null. [switch] $NotNull, # Requires parameter to be non-null and non-empty. [switch] $NotNullOrEmpty, # Requires the parameter value to be Trusted data. [switch] $TrustedData, # Requires a path parameter to be on a User drive. [switch] $UserDrive, # Indicates a required parameter. [Alias('Required')][switch] $Mandatory, # Indicates a parameter that can accept values from the pipeline. [Alias('Pipeline')][switch] $ValueFromPipeline, <# Indicates a parameter that can accept values from the pipeline by matching the property name of pipeline objects to the parameter name or alias. #> [Alias('PipelineProperties','PipeName')][switch] $ValueFromPipelineByPropertyName, # Indicates that the parameter will include any following positional parameters. [Alias('RemainingArgs')][switch] $ValueFromRemainingArguments ) End { $DynamicParams = Get-Variable DynamicParams -Scope 1 -ErrorAction Ignore if($null -eq $DynamicParams) { $DynamicParams = New-Object Management.Automation.RuntimeDefinedParameterDictionary $DynamicParams = New-Variable DynamicParams $DynamicParams -Scope 1 -PassThru } $atts = New-Object Collections.ObjectModel.Collection[System.Attribute] foreach($set in $ParameterSetName) { $att = New-Object Management.Automation.ParameterAttribute -Property @{ Position = $Position ParameterSetName = $ParameterSetName Mandatory = $Mandatory ValueFromPipeline = $ValueFromPipeline ValueFromPipelineByPropertyName = $ValueFromPipelineByPropertyName ValueFromRemainingArguments = $ValueFromRemainingArguments } $atts.Add($att) } if($Alias) {$atts.Add((New-Object Management.Automation.AliasAttribute $Alias))} if($NotNull) {$atts.Add((New-Object Management.Automation.ValidateNotNullAttribute))} if($NotNullOrEmpty) {$atts.Add((New-Object Management.Automation.ValidateNotNullOrEmptyAttribute))} if($ValidateCount) { if($ValidateCount.Length -eq 1) {$ValidateCount += $ValidateCount[0]} $atts.Add((New-Object Management.Automation.ValidateCountAttribute $ValidateCount)) } if($ValidateDrive) {$atts.Add((New-Object Management.Automation.ValidateDriveAttribute $ValidateDrive))} if($ValidateLength) { if($ValidateLength.Length -eq 1) {$ValidateLength += $ValidateLength[0]} $atts.Add((New-Object Management.Automation.ValidateLengthAttribute $ValidateLength)) } if($ValidatePattern) {$atts.Add((New-Object Management.Automation.ValidatePatternAttribute $ValidatePattern))} if($ValidateRange) {$atts.Add((New-Object Management.Automation.ValidateRangeAttribute $ValidateRange))} if($ValidateScript) {$atts.Add((New-Object Management.Automation.ValidateScriptAttribute $ValidateScript))} [psobject[]] $ValidateSet = $input if($ValidateSet) {$atts.Add((New-Object Management.Automation.ValidateSetAttribute $ValidateSet))} if($TrustedData) {$atts.Add((New-Object Management.Automation.ValidateTrustedDataAttribute))} if($UserDrive) {$atts.Add((New-Object Management.Automation.ValidateUserDriveAttribute))} $param = New-Object Management.Automation.RuntimeDefinedParameter ($Name,$Type,$atts) $DynamicParams.Value.Add($Name,$param) } } function Export-SecretVault { <# .SYNOPSIS Exports secret vault content. .OUTPUTS System.Management.Automation.PSObject with these fields: * Name: The secret name, used to identify the secret. * Type: The data type of the secret. * VaultName: Which vault the secret is stored in. * Metadata: A simple hash (string to string/int/datetime) of extra secret context details. .FUNCTIONALITY Credential .LINK https://devblogs.microsoft.com/powershell/secretmanagement-and-secretstore-are-generally-available/ .EXAMPLE Export-SecretVault |ConvertTo-Json |Out-File ~/secrets.json utf8 Backs up all secrets to a JSON file. #> [CmdletBinding(ConfirmImpact='High',SupportsShouldProcess=$true)] Param() filter Export-Credential { [CmdletBinding()][OutputType([pscustomobject])] Param( [Parameter(ValueFromPipelineByPropertyName=$true)][string] $UserName, [Parameter(ValueFromPipelineByPropertyName=$true)][securestring] $Password ) return [pscustomobject]@{ UserName = $UserName Password = $Password |ConvertFrom-SecureString -AsPlainText } } filter Export-Secret { [CmdletBinding()][OutputType([pscustomobject])] Param( [Parameter(ValueFromPipelineByPropertyName=$true)][string] $Name, [Parameter(ValueFromPipelineByPropertyName=$true)][string] $Type, [Parameter(ValueFromPipelineByPropertyName=$true)][string] $VaultName, [Parameter(ValueFromPipelineByPropertyName=$true)] [Collections.ObjectModel.ReadOnlyDictionary[string,object]] $Metadata ) return [pscustomobject]@{ Name = $Name Type = $Type Value = switch($Type) { ByteArray {[Convert]::ToHexString((Get-Secret $Name -Vault $VaultName))} String {Get-Secret $Name -Vault $VaultName -AsPlainText} SecureString {Get-Secret $Name -Vault $VaultName -AsPlainText} PSCredential {Get-Secret $Name -Vault $VaultName |Export-Credential} Hashtable {Get-Secret $Name -Vault $VaultName -AsPlainText} # not yet supported default {Get-Secret $Name -Vault $VaultName -AsPlainText} } Vault = $VaultName Metadata = $Metadata } } if(!$PSCmdlet.ShouldProcess('secret vaults','export')) {return} return @(Get-SecretInfo |Export-Secret) } function Get-SecretDetails { <# .SYNOPSIS Returns secret info from the secret vaults, including metadata as properties. .PARAMETER Name This parameter takes a String argument, including wildcard characters. It is used to filter the search results that match on secret names the provided name pattern. If no Name parameter argument is provided, then all stored secret metadata is returned. .PARAMETER Vault Optional parameter which takes a String argument that specifies a single vault to search. .FUNCTIONALITY Credential .EXAMPLE Get-SecretDetails Name : test-creds Type : PSCredential VaultName : SecretStore Title : Test Description : Example credentials. Note : Just for testing. Uri : https://example.org/ Created : 2024-12-31 00:00:00 Expires : 2036-01-01 00:00:00 #> [CmdletBinding()] Param() DynamicParam { Get-SecretInfo |Select-Object -ExpandProperty Name |Add-DynamicParam Name string -Position 0 Get-SecretVault |Select-Object -ExpandProperty Name |Add-DynamicParam Vault string -Position 1 $DynamicParams } Process { Get-SecretInfo @PSBoundParameters | ForEach-Object {[pscustomobject][ordered]@{ Name = $_.Name Type = $_.Type VaultName = $_.VaultName Title = $_.Metadata['Title'] Description = $_.Metadata['Description'] Note = $_.Metadata['Note'] Uri = $_.Metadata['Uri'] Created = $_.Metadata['Created'] Expires = $_.Metadata['Expires'] }} } } function Import-SecretVault { <# .SYNOPSIS Imports secrets into secret vaults. .NOTES This is likely the configuration you'll need to run this: Set-SecretStoreConfiguration -Scope CurrentUser -Authentication None -Interaction None .INPUTS System.Management.Automation.PSObject with these fields: * Name: The secret name, used to identify the secret. * Type: The data type of the secret. * VaultName: Which vault the secret is stored in. * Metadata: A simple hash (string to string/int/datetime) of extra secret context details. .FUNCTIONALITY Credential .LINK https://devblogs.microsoft.com/powershell/secretmanagement-and-secretstore-are-generally-available/ .EXAMPLE Get-Content ~/secrets.json |ConvertFrom-Json |Import-SecretVault Restores secrets to vaults. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText','', Justification='This script exports secrets.')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword','', Justification='This script exports secrets.')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingUsernameAndPasswordParams','', Justification='This script exports secrets.')] [CmdletBinding(ConfirmImpact='High',SupportsShouldProcess=$true)] Param( [Parameter(ValueFromPipelineByPropertyName=$true)][string] $Name, [Parameter(ValueFromPipelineByPropertyName=$true)][string] $Type, [Parameter(ValueFromPipelineByPropertyName=$true)][psobject] $Value, [Parameter(ValueFromPipelineByPropertyName=$true)][string] $Vault, [Parameter(ValueFromPipelineByPropertyName=$true)][psobject] $Metadata ) Begin { filter ConvertTo-Credential { [CmdletBinding()] Param( [Parameter(ValueFromPipelineByPropertyName=$true)][string] $UserName, [Parameter(ValueFromPipelineByPropertyName=$true)][string] $Password ) return New-Object PSCredential $UserName,(ConvertTo-SecureString $Password -AsPlainText -Force) } } Process { if(!(Get-SecretVault $Vault -ErrorAction Ignore)) { Register-SecretVault -Name $Vault -ModuleName Microsoft.PowerShell.SecretStore } $meta = @($Metadata.PSObject.Properties).Count ? @{Metadata=$Metadata |ConvertTo-Json |ConvertFrom-Json -AsHashtable} : @{} foreach($k in $meta.Keys) {if($meta[$k] -is [long]){$meta[$k] = [int]$meta[$k]}} switch($Type) { ByteArray {Set-Secret $Name ([Convert]::FromHexString($Value)) -Vault $Vault @meta} String {Set-Secret $Name $Value -Vault $Vault @meta} SecureString {Set-Secret $Name (ConvertTo-SecureString $Value -AsPlainText -Force) -Vault $Vault @meta} PSCredential {Set-Secret $Name ($Value |ConvertTo-Credential) -Vault $Vault @meta} #Hashtable {Set-Secret $Name ($Value |ConvertTo-Json |ConvertFrom-Json -AsHashtable) -Vault $Vault @meta} # not yet supported default {Set-Secret $Name $Value -Vault $Vault @meta} } } } function Initialize-SecretVault { <# .SYNOPSIS Sets up secret storage for the first time. .FUNCTIONALITY Credential .LINK https://devblogs.microsoft.com/powershell/secretmanagement-and-secretstore-are-generally-available/ .EXAMPLE Initialize-SecretVault #> #Requires -Version 7 [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='High')] Param( # Disable password prompts [Alias('Headless')][switch] $HandsFree ) if(Test-SecretVault) {Write-Information 'Secret vault is already set up.'; return} if(!$HandsFree) {Set-SecretStoreConfiguration -Default} else { if(!$PSCmdlet.ShouldProcess('secret vault','set up without a password')) {return} Set-SecretStoreConfiguration -Authentication None -Interaction None } Register-SecretVault -Name SecretVault -ModuleName Microsoft.PowerShell.SecretStore -DefaultVault if(Test-SecretVault) {Write-Information 'Secret vault has been set up!'} } function Set-SecretDetails { <# .SYNOPSIS Sets a secret in a secret vault with metadata. .FUNCTIONALITY Credential .EXAMPLE Set-SecretDetails GitHubToken -Paste securestring -Title 'PowerShell token' -Description 'A GitHub classic token' -Url https://github.com/settings/tokens -Expires (Get-Date).AddDays(90) Stores the token from the clipboard. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText','', Justification='The data source is plaintext. SecureString benefits may be in dispute: <https://github.com/dotnet/platform-compat/blob/master/docs/DE0001.md>')] [CmdletBinding()] Param( # Specifies the name of the secret to add metadata to. Wildcard characters (`*`) are not permitted. [Parameter(Position=0,Mandatory=$true)][string] $Name, # Specifies the value of the secret. [Parameter(ParameterSetName='Secret',Position=1,Mandatory=$true)][securestring] $Secret, # Specifies the value of the credential to store. [Parameter(ParameterSetName='Credential',Position=1,Mandatory=$true)][pscredential] $Credential, # Title metadata field. [string] $Title, # Description metadata field. [string] $Description, # Note metadata field. [string] $Note, # Uri metadata field. [uri] $Uri, # Created date/time metadata field. [datetime] $Created = (Get-Date), # Expiration date/time metadata field. [datetime] $Expires, # Specifies the type to interpret the text on the clipboard as for use as the secret value. [Parameter(ParameterSetName='Paste',Mandatory=$true)] [ValidateSet('string','securestring','bytes','hexbytes')][string] $Paste, # Specifies the username to combine with the clipboard text as a password to store as a credential secret. [Parameter(ParameterSetName='PasteForUser',Mandatory=$true)][string] $PasteForUser, # Specifies the encoding to read the clipboard as, into a byte array secret. [Parameter(ParameterSetName='PasteTextBytes',Mandatory=$true)][Text.Encoding] $PasteTextBytes ) $clipboard = Get-Clipboard |Out-String $value = switch($PSCmdlet.ParameterSetName) { PasteForUser {New-Object pscredential $PasteForUser,($clipboard |ConvertTo-SecureString -AsPlainText -Force)} PasteTextBytes {$PasteTextBytes.GetBytes($clipboard)} Secret {$Secret} Credential {$Credential} Paste { switch($Paste) { string {$clipboard} securestring {$clipboard |ConvertTo-SecureString -AsPlainText -Force} bytes {[byte[]]($clipboard -split '\D+')} hexbytes { $clipboard = $clipboard -replace '[^A-Fa-f0-9]+' $bytes = [bigint]::Parse("00$clipboard",'HexNumber').ToByteArray() [array]::Reverse($bytes) $null,$bytes = $bytes $bytes } } } } $metadata = @{ Created = $Created } if($Title) {$metadata['Title'] = $Title} if($Description) {$metadata['Description'] = $Description} if($Note) {$metadata['Note'] = $Note} if($Uri) {$metadata['Uri'] = "$Uri"} if($Expires) {$metadata['Expires'] = $Expires} Set-Secret $Name $value -Metadata $metadata } Export-ModuleMember -Function Export-SecretVault,Get-SecretDetails,Import-SecretVault,Initialize-SecretVault,Set-SecretDetails |