SecretManagement.CyberArk.Extension/SecretManagement.CyberArk.Extension.psm1
|
function Get-Secret { [CmdletBinding()] param ( [string] $Name, [string] $VaultName, [hashtable] $AdditionalParameters, [Parameter(ValueFromPipeline = $true)] [object] $PASAccount ) switch ($AdditionalParameters.ConnectionType) { 'CredentialProvider' { $Credential = Invoke-GetAIMCredential -Name $Name -AdditionalParameters $AdditionalParameters if ($null -ne $Credential) { $Credential = ConvertTo-ProviderCredential -Result $Credential -SecretProperty 'Password' -Name $Name if ($null -eq $Credential) { return } } } 'CentralCredentialProvider' { $Credential = Invoke-GetCCPCredential -Name $Name -AdditionalParameters $AdditionalParameters if ($null -ne $Credential) { $Credential = ConvertTo-ProviderCredential -Result $Credential -SecretProperty 'Content' -Name $Name if ($null -eq $Credential) { return } } } 'REST' { if (-not (Test-PASSession)) { return } if ($PASAccount) { $VaultSafe = Get-CaseInsensitiveValue -Table $AdditionalParameters -Key 'safeName' try { $Account = Get-PASAccount -id $PASAccount.Id } catch { Write-Error "Failed to look up account '$Name': $($_.Exception.Message)" return } if ($VaultSafe -and -not ($Account.safeName -ieq $VaultSafe)) { Write-Error "Account '$Name' is not in the vault's safe '$VaultSafe'. Not retrieving the password." return } } else { $GetPASAccountParameters = @{ search = $Name } $VaultSafe = Get-CaseInsensitiveValue -Table $AdditionalParameters -Key 'safeName' if ($VaultSafe) { $GetPASAccountParameters.Add('safeName', $VaultSafe) } try { $results = @(Get-PASAccount @GetPASAccountParameters | Where-Object { $_.name -eq $Name } | Select-AccountInVaultSafe -SafeName $VaultSafe) } catch { Write-Error "Failed to look up account '$Name': $($_.Exception.Message)" return } if ($results.Count -eq 0) { return $null } if ($results.Count -gt 1) { $message = "Found $($results.Count) accounts named '$Name'. Use a unique name or the PASAccount parameter." Write-Error $message return } $Account = $results[0] } $GetPASAccountPasswordParameters = @{ AccountId = $Account.Id } if ($AdditionalParameters.Reason) { $GetPASAccountPasswordParameters.Add('Reason', $AdditionalParameters.Reason) } if ($AdditionalParameters.TicketingSystem) { $GetPASAccountPasswordParameters.Add('TicketingSystem', $AdditionalParameters.TicketingSystem) } if ($AdditionalParameters.TicketId) { $GetPASAccountPasswordParameters.Add('TicketId', $AdditionalParameters.TicketId) } if ($AdditionalParameters.Version) { $GetPASAccountPasswordParameters.Add('Version', $AdditionalParameters.Version) } if ($AdditionalParameters.ActionType) { $GetPASAccountPasswordParameters.Add('ActionType', $AdditionalParameters.ActionType) } if ($AdditionalParameters.isUse) { $GetPASAccountPasswordParameters.Add('isUse', $AdditionalParameters.isUse) } if ($AdditionalParameters.Machine) { $GetPASAccountPasswordParameters.Add('Machine', $AdditionalParameters.Machine) } try { $AccountSecret = Get-PASAccountPassword @GetPASAccountPasswordParameters $Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $Account.userName, $AccountSecret.ToSecureString() } catch { Write-Error "Failed to retrieve the password for account '$Name': $($_.Exception.Message)" return } } default { Write-Error "ConnectionType $($AdditionalParameters.ConnectionType) is not supported" return } } return $Credential } function ConvertTo-ProviderCredential { # Validates a Get-AIMCredential/Get-CCPCredential result and converts it. Never reports secret values. param ($Result, [string] $SecretProperty, [string] $Name) if ([string]::IsNullOrEmpty("$($Result.UserName)")) { Write-Error "The provider returned no UserName for '$Name'; cannot build a credential." return } if ([string]::IsNullOrEmpty("$($Result.$SecretProperty)")) { Write-Error "The provider returned an empty $SecretProperty for '$Name'; cannot build a credential." return } try { return $Result.ToCredential() } catch { Write-Error "Failed to build a credential for '$Name': $($_.Exception.Message)" return } } function Get-SecretInfo { [CmdletBinding()] param ( [string] $Filter, [string] $VaultName, [hashtable] $AdditionalParameters ) if ([string]::IsNullOrEmpty($Filter)) { $Filter = '*' } $VerboseOn = [bool]$AdditionalParameters.Verbose # Non-secret, useful fields only. Anything else (secrets, nested objects) is never copied into metadata. $ProviderKeys = @('Name', 'UserName', 'Address', 'Safe', 'Folder', 'PolicyID', 'DeviceType') $RestKeys = @('id', 'name', 'userName', 'address', 'safeName', 'platformId', 'secretType', 'createdTime') $SecretDataType = [Microsoft.PowerShell.SecretManagement.SecretType]::PSCredential switch ($AdditionalParameters.ConnectionType) { { $_ -in 'CredentialProvider', 'CentralCredentialProvider' } { if ([WildcardPattern]::ContainsWildcardCharacters($Filter)) { Write-Verbose "$($AdditionalParameters.ConnectionType) cannot enumerate secrets, so wildcard filter '$Filter' returns nothing. Specify the exact object name." -Verbose:$VerboseOn return } $Keys = $ProviderKeys if ($_ -eq 'CredentialProvider') { # Get-Secret looks up by UserName, so report that name (not the object Name) to keep it round-trippable $ReportLookupName = $true $results = Invoke-GetAIMCredential -Name $Filter -AdditionalParameters $AdditionalParameters } else { $results = Invoke-GetCCPCredential -Name $Filter -AdditionalParameters $AdditionalParameters } } 'REST' { if (-not (Test-PASSession)) { return } $Keys = $RestKeys # Search with the longest non-wildcard fragment, then apply the real wildcard match locally. # Character classes and backtick escapes make the literal fragments unreliable, so skip the prefilter then. $SearchText = $null if ($Filter -notmatch '[\[`]') { $SearchText = [regex]::Split($Filter, '[*?\[\]]') | Sort-Object -Property Length -Descending | Select-Object -First 1 } $GetPASAccountParameters = @{} if ($SearchText) { $GetPASAccountParameters.search = $SearchText } $VaultSafe = Get-CaseInsensitiveValue -Table $AdditionalParameters -Key 'safeName' if ($VaultSafe) { $GetPASAccountParameters.safeName = $VaultSafe } try { $results = @(Get-PASAccount @GetPASAccountParameters | Where-Object { $_.name -like $Filter } | Select-AccountInVaultSafe -SafeName $VaultSafe) } catch { Write-Error "Failed to look up accounts matching '$Filter': $($_.Exception.Message)" return } } Default { Write-Error "ConnectionType $($AdditionalParameters.ConnectionType) is not supported" return } } $Secrets = New-Object System.Collections.Generic.List[System.Object] foreach ($Account in $results) { $Metadata = [Ordered]@{} foreach ($Key in $Keys) { $Property = $Account.psobject.Properties[$Key] if ($null -eq $Property) { continue } $Value = ConvertTo-SecretMetadataValue -Key $Key -Value $Property.Value if ($null -ne $Value) { $Metadata[$Key] = $Value } } $SecretName = if ($ReportLookupName -or -not $Account.name) { $Filter } else { "$($Account.name)" } $SecretInfo = [Microsoft.PowerShell.SecretManagement.SecretInformation]::new( $SecretName, # Name of secret $SecretDataType, # Secret data type [Microsoft.PowerShell.SecretManagement.SecretType] $VaultName, # Name of vault $Metadata) # Optional Metadata parameter) $Secrets.Add($SecretInfo) } return $Secrets } function ConvertTo-SecretMetadataValue { # SecretManagement metadata supports string, int and DateTime only. param ([string] $Key, $Value) if ($null -eq $Value) { return $null } if ($Key -eq 'createdTime' -and $Value -isnot [datetime]) { # psPAS leaves REST times as Unix epoch seconds (milliseconds above 1e11); anything else is dropped, never thrown on if ($Value -isnot [int] -and $Value -isnot [long] -and $Value -isnot [double] -and $Value -isnot [decimal]) { return $null } try { if ([math]::Abs([double]$Value) -gt 1e11) { return [DateTimeOffset]::FromUnixTimeMilliseconds([long]$Value).UtcDateTime } return [DateTimeOffset]::FromUnixTimeSeconds([long]$Value).UtcDateTime } catch { return $null } } if ($Value -is [string] -or $Value -is [datetime]) { return $Value } if ($Value -is [int]) { return $Value } if ($Value -is [long] -or $Value -is [int16] -or $Value -is [byte]) { if ($Value -ge [int]::MinValue -and $Value -le [int]::MaxValue) { return [int]$Value } return "$Value" } return $null } function Remove-Secret { [CmdletBinding()] param ( [string] $Name, [string] $VaultName, [hashtable] $AdditionalParameters ) switch ($AdditionalParameters.ConnectionType) { 'CredentialProvider' { Write-Error 'Remove-Secret is not supported for Credential Provider' return } 'CentralCredentialProvider' { Write-Error 'Remove-Secret is not supported for Central Credential Provider' return } 'REST' { if (-not (Test-PASSession)) { return } $GetPASAccountParameters = @{ search = "$Name" } $VaultSafe = Get-CaseInsensitiveValue -Table $AdditionalParameters -Key 'safeName' if ($VaultSafe) { $GetPASAccountParameters.safeName = $VaultSafe } try { $results = @(Get-PASAccount @GetPASAccountParameters | Where-Object { $PSItem.name -eq $Name } | Select-AccountInVaultSafe -SafeName $VaultSafe) } catch { Write-Error "Failed to look up account '$Name': $($_.Exception.Message)" return } if ($results.Count -eq 0) { Write-Error "No account found with name $Name. Not deleting anything." return } elseif ($results.Count -gt 1) { Write-Error "Multiple accounts found with name $Name. Not deleting anything." return } try { Remove-PASAccount -id $results[0].Id } catch { Write-Error "Failed to remove account '$Name': $($_.Exception.Message)" return } } Default { Write-Error "ConnectionType $($AdditionalParameters.ConnectionType) is not supported" return } } } function Set-Secret { [CmdletBinding()] param ( [string] $Name, [object] $Secret, [string] $VaultName, [hashtable] $AdditionalParameters, [hashtable] $Metadata ) switch ($AdditionalParameters.ConnectionType) { 'CredentialProvider' { Write-Error 'Set-Secret is not supported for Credential Provider' return } 'CentralCredentialProvider' { Write-Error 'Set-Secret is not supported for Central Credential Provider' return } 'REST' { $Known = 'SafeName', 'PlatformId', 'address', 'userName' $Resolved = @{} foreach ($Key in $Known) { $Value = Get-CaseInsensitiveValue -Table $Metadata -Key $Key if ($null -eq $Value) { $Value = Get-CaseInsensitiveValue -Table $AdditionalParameters -Key $Key } if ($null -ne $Value) { $Resolved[$Key] = $Value } } $Unknown = @($Metadata.Keys | Where-Object { $_ -notin $Known }) if ($Unknown) { Write-Verbose "Ignoring unsupported Metadata keys: $($Unknown -join ', ')" -Verbose:($AdditionalParameters.Verbose -eq $true -or $VerbosePreference -eq 'Continue') } $Missing = @('SafeName', 'PlatformId' | Where-Object { -not $Resolved.ContainsKey($_) }) if ($Missing) { Write-Error "SafeName and PlatformId are required (via -Metadata or VaultParameters); missing: $($Missing -join ', ')" return } # psPAS -secret takes a SecureString; accept what SecretManagement hands over (PSCredential from Get-Secret, string) $SecretValue = $Secret if ($Secret -is [pscredential]) { $SecretValue = $Secret.Password if (-not $Resolved.ContainsKey('userName') -and -not [string]::IsNullOrEmpty($Secret.UserName)) { $Resolved['userName'] = $Secret.UserName } } elseif ($Secret -is [string]) { $SecretValue = ConvertTo-SecureString -String $Secret -AsPlainText -Force } elseif ($null -ne $Secret -and $Secret -isnot [securestring]) { Write-Error "Set-Secret: unsupported secret type $($Secret.GetType().FullName) for REST; use a SecureString, string or PSCredential" return } if (-not (Test-PASSession)) { return } $AddPASAccountParameters = @{ SafeName = $Resolved.SafeName PlatformId = $Resolved.PlatformId } if ($Name) { $AddPASAccountParameters.Add('name', $Name) } if ($Resolved.ContainsKey('userName')) { $AddPASAccountParameters.Add('userName', $Resolved.userName) } if ($Resolved.ContainsKey('address')) { $AddPASAccountParameters.Add('address', $Resolved.address) } if ($SecretValue) { $AddPASAccountParameters.Add('secret', $SecretValue) } try { Add-PASAccount @AddPASAccountParameters } catch { Write-Error "Failed to add account '$Name': $($_.Exception.Message)" return } } Default { Write-Error "ConnectionType $($AdditionalParameters.ConnectionType) is not supported" return } } } function Select-AccountInVaultSafe { # Enforces the vault's SafeName scope client-side too, so it holds even if the server-side filter does not. param ([Parameter(ValueFromPipeline)] $Account, [string] $SafeName) process { if ([string]::IsNullOrEmpty($SafeName) -or $Account.safeName -ieq $SafeName) { $Account } } } function Get-CaseInsensitiveValue { # Returns the first non-empty value whose key equals $Key ignoring case, else $null. param ([hashtable] $Table, [string] $Key) foreach ($K in $Table.Keys) { if ($K -ieq $Key -and -not [string]::IsNullOrEmpty("$($Table[$K])")) { return $Table[$K] } } return $null } function Test-SecretVault { [CmdletBinding()] param ( [Parameter(ValueFromPipelineByPropertyName)] [string] $VaultName, [Parameter(ValueFromPipelineByPropertyName)] [hashtable] $AdditionalParameters ) switch ($AdditionalParameters.ConnectionType) { 'CredentialProvider' { if (-not $AdditionalParameters.AppID) { Write-Error 'CredentialProvider vault parameter AppID is missing'; return $false } if (-not (Get-Command -Name Get-AIMCredential -ErrorAction SilentlyContinue)) { Write-Error 'Get-AIMCredential command not found'; return $false } return $true } 'CentralCredentialProvider' { if (-not $AdditionalParameters.AppID) { Write-Error 'CentralCredentialProvider vault parameter AppID is missing'; return $false } if (-not $AdditionalParameters.URL) { Write-Error 'CentralCredentialProvider vault parameter URL is missing'; return $false } if (-not (Get-Command -Name Get-CCPCredential -ErrorAction SilentlyContinue)) { Write-Error 'Get-CCPCredential command not found'; return $false } return $true } 'REST' { return (Test-PASSession) } Default { Write-Error "ConnectionType $($AdditionalParameters.ConnectionType) is not supported" return $false } } } function Test-PASSession { try { if (-not (Get-PASSession).BaseURI) { throw 'No active session' } } catch { Write-Error 'Failed to get PASSession. Run New-PASSession again.' return $false } return $true } function Invoke-GetCCPCredential { [CmdletBinding()] param ( [string] $Name, [hashtable] $AdditionalParameters ) # Vault-level, JSON-persistable Get-CCPCredential connection settings (CredentialRetriever 4.0.0). # Credential/Certificate cannot survive VaultParameters persistence; Safe/Folder/UserName/Address/Database/ # PolicyID/Query/QueryFormat/Reason identify a secret; AsCredential/AsSecureString would change the return type. $AllowedKeys = @( 'AppID', 'URL', 'SkipCertificateCheck', 'UseDefaultCredentials', 'CertificateThumbPrint', 'WebServiceName', 'ConnectionTimeout', 'Method', 'FailRequestOnPasswordChange' ) $SilentKeys = @('ConnectionType', 'Verbose') # Get-CCPCredential acts on a switch being bound, even as $false (UseDefaultCredentials would send OS credentials), # so switches are passed only when truthy. $SwitchKeys = @('SkipCertificateCheck', 'UseDefaultCredentials', 'FailRequestOnPasswordChange') $GetCCPCredentialParameters = @{} $IgnoredKeys = @() foreach ($Key in $AdditionalParameters.Keys) { if ($Key -in $AllowedKeys) { $CanonicalKey = $AllowedKeys | Where-Object { $_ -eq $Key } $Value = $AdditionalParameters[$Key] if ($CanonicalKey -in $SwitchKeys) { $Parsed = $false if ($Value -is [string] -and [bool]::TryParse($Value, [ref]$Parsed)) { $Value = $Parsed } if (-not $Value) { continue } $Value = $true } $GetCCPCredentialParameters[$CanonicalKey] = $Value } elseif ($Key -notin $SilentKeys) { $IgnoredKeys += $Key } } if ($IgnoredKeys) { Write-Warning "Ignoring unsupported vault parameter(s) for CentralCredentialProvider: $($IgnoredKeys -join ', ')" } $GetCCPCredentialParameters.Add('Object', $Name) try { $Credential = Get-CCPCredential @GetCCPCredentialParameters } catch { Write-Error "Failed to retrieve '$Name' from the Central Credential Provider: $($_.Exception.Message)" return } return $Credential } function Invoke-GetAIMCredential { [CmdletBinding()] param ( [string] $Name, [hashtable] $AdditionalParameters ) try { if ($AdditionalParameters.ClientPath) { Set-AIMConfiguration -ClientPath $AdditionalParameters.ClientPath } $GetAIMCredentialParameters = @{ AppID = $AdditionalParameters.AppID UserName = $Name } # Optional vault-level scope; Get-AIMCredential's parameter is named Safe if ($AdditionalParameters.SafeName) { $GetAIMCredentialParameters.Add('Safe', $AdditionalParameters.SafeName) } # UserName is always requested so a PSCredential can be built $RequiredProps = @($AdditionalParameters.RequiredProps) + 'UserName' | Where-Object { $_ } | Select-Object -Unique $GetAIMCredentialParameters.Add('RequiredProps', $RequiredProps) $Credential = Get-AIMCredential @GetAIMCredentialParameters } catch { Write-Error "Failed to retrieve '$Name' from the Credential Provider: $($_.Exception.Message)" return } return $Credential } |