├── .gitignore ├── PSCredentialManager.Tests.ps1 ├── PSCredentialManager.psd1 ├── PSCredentialManager.psm1 ├── README.md ├── appveyor.yml └── buildscripts ├── build.ps1 ├── install.ps1 ├── publish.ps1 └── test.ps1 /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /PSCredentialManager.Tests.ps1: -------------------------------------------------------------------------------- 1 | #region import modules 2 | $ThisModule = "$($MyInvocation.MyCommand.Path -replace '\.Tests\.ps1$', '').psd1" 3 | $ThisModuleName = (($ThisModule | Split-Path -Leaf) -replace '\.psd1') 4 | Get-Module -Name $ThisModuleName -All | Remove-Module -Force 5 | 6 | Import-Module -Name $ThisModule -Force -ErrorAction Stop 7 | #endregion 8 | 9 | describe 'Module-level tests' { 10 | 11 | it 'should validate the module manifest' { 12 | 13 | { Test-ModuleManifest -Path $ThisModule -ErrorAction Stop } | should not throw 14 | } 15 | 16 | it 'should pass all analyzer rules' { 17 | 18 | $excludedRules = @( 19 | 'PSUseShouldProcessForStateChangingFunctions', 20 | 'PSUseToExportFieldsInManifest', 21 | 'PSAvoidInvokingEmptyMembers', 22 | 'PSUsePSCredentialType', 23 | 'PSAvoidUsingPlainTextForPassword' 24 | 'PSAvoidUsingConvertToSecureStringWithPlainText' 25 | ) 26 | 27 | Invoke-ScriptAnalyzer -Path $PSScriptRoot -ExcludeRule $excludedRules -Severity Error | Select-Object -ExpandProperty RuleName | should benullorempty 28 | } 29 | } 30 | 31 | InModuleScope $ThisModuleName { 32 | 33 | 34 | } -------------------------------------------------------------------------------- /PSCredentialManager.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | RootModule = 'PSCredentialManager.psm1' 3 | ModuleVersion = '1.0' 4 | GUID = '1edda3dd-4527-46c4-9d86-f6bda0dc1390' 5 | Author = 'Adam Bertram' 6 | CompanyName = 'Adam the Automator, LLC' 7 | Copyright = '(c) 2017 Adam Bertram. All rights reserved.' 8 | Description = 'This module allows management and automation of Windows cached credentials.' 9 | PowerShellVersion = '5.0' 10 | FunctionsToExport = '*' 11 | CmdletsToExport = '*' 12 | VariablesToExport = '*' 13 | AliasesToExport = '*' 14 | PrivateData = @{ 15 | PSData = @{ 16 | 17 | # Tags applied to this module. These help with module discovery in online galleries. 18 | Tags = @('PSModule') 19 | 20 | # A URL to the license for this module. 21 | # LicenseUri = '' 22 | 23 | # A URL to the main website for this project. 24 | ProjectUri = 'https://github.com/adbertram/PSCredentialManager' 25 | 26 | # A URL to an icon representing this module. 27 | # IconUri = '' 28 | 29 | # ReleaseNotes of this module 30 | # ReleaseNotes = '' 31 | 32 | } # End of PSData hashtable 33 | 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /PSCredentialManager.psm1: -------------------------------------------------------------------------------- 1 | function Get-CachedCredential 2 | { 3 | [OutputType([pscustomobject])] 4 | [CmdletBinding()] 5 | param 6 | ( 7 | [Parameter()] 8 | [ValidateNotNullOrEmpty()] 9 | [string[]]$ComputerName, 10 | 11 | [Parameter()] 12 | [ValidateNotNullOrEmpty()] 13 | [string]$TargetName 14 | ) 15 | 16 | if (-not $PSBoundParameters.ContainsKey('ComputerName') -and -not ($PSBoundParameters.ContainsKey('Name'))) 17 | { 18 | ConvertTo-CachedCredential -CmdKeyOutput (cmdkey /list) 19 | } elseif (-not $PSBoundParameters.ContainsKey('ComputerName') -and $PSBoundParameters.ContainsKey('Name')) { 20 | ConvertTo-CachedCredential -CmdKeyOutput (cmdkey /list:$TargetName) 21 | } else { 22 | foreach ($c in $ComputerName) { 23 | $cmdkeyOutput = Invoke-PsExec -ComputerName $c -Command 'cmdkey /list' 24 | if ($cred = ConvertTo-CachedCredential -CmdKeyOutput $cmdkeyOutput) { 25 | [pscustomobject]@{ 26 | ComputerName = $c 27 | Credentials = $cred 28 | } 29 | } 30 | } 31 | } 32 | } 33 | 34 | function Install-PsExec 35 | { 36 | [OutputType([void])] 37 | [CmdletBinding()] 38 | param 39 | ( 40 | [Parameter()] 41 | [ValidateNotNullOrEmpty()] 42 | [string]$Uri = 'https://download.sysinternals.com/files/PSTools.zip' 43 | ) 44 | 45 | $zipPath = "$env:TEMP\PSTools.zip" 46 | $folder = "$env:TEMP\PSTools" 47 | if (-not (Test-Path -Path $zipPath -PathType Container)) { 48 | Invoke-WebRequest -Uri $Uri -UseBasicParsing -OutFile $zipPath 49 | Expand-Archive -Path $zipPath -DestinationPath $folder 50 | } 51 | $null = & "$folder\psexec.exe" -accepteula 52 | 53 | } 54 | 55 | function Invoke-PsExec 56 | { 57 | [CmdletBinding()] 58 | param 59 | ( 60 | [Parameter(Mandatory)] 61 | [ValidateNotNullOrEmpty()] 62 | [string]$ComputerName, 63 | 64 | [Parameter(Mandatory)] 65 | [ValidateNotNullOrEmpty()] 66 | [string]$Command, 67 | 68 | [Parameter()] 69 | [ValidateNotNullOrEmpty()] 70 | [pscredential]$Credential = (Get-Credential -Message 'Enter credential to authenticate to remote computer(s).' -UserName (whoami)) 71 | ) 72 | 73 | try { 74 | 75 | if (-not (Test-PsExecInstalled)) { 76 | Install-PsExec 77 | } 78 | 79 | $x = $Command -split ' ' 80 | $cmd = $x[0] 81 | $cmdArgs = $x[1..($x.Length)] 82 | 83 | $startParams = @{ 84 | FilePath = "$env:temp\pstools\psexec.exe" 85 | Wait = $true 86 | NoNewWindow = $true 87 | ArgumentList = "\\$ComputerName -user $($Credential.UserName) -pass $($Credential.GetNetworkCredential().Password) $cmd $cmdArgs" 88 | RedirectStandardError = "$env:TEMP\err.txt" 89 | RedirectStandardOutput = "$env:TEMP\out.txt" 90 | } 91 | Start-Process @startParams 92 | Get-Content -Path "$env:TEMP\out.txt" -Raw 93 | } catch { 94 | $PSCmdlet.ThrowTerminatingError($_) 95 | } finally { 96 | @("$env:TEMP\err.txt","$env:TEMP\out.txt").foreach({ 97 | Remove-Item -Path $_ -ErrorAction Ignore 98 | }) 99 | } 100 | } 101 | 102 | function Test-PsExecInstalled 103 | { 104 | [OutputType([bool])] 105 | [CmdletBinding()] 106 | param 107 | () 108 | if (-not (Test-Path -Path "$env:TEMP\PSTools\psexec.exe" -PathType Leaf)) { 109 | $false 110 | } else { 111 | $true 112 | } 113 | } 114 | 115 | function Remove-CachedCredential 116 | { 117 | [OutputType([void])] 118 | [CmdletBinding()] 119 | param 120 | ( 121 | [Parameter(Mandatory)] 122 | [ValidateNotNullOrEmpty()] 123 | [string]$TargetName, 124 | 125 | [Parameter()] 126 | [ValidateNotNullOrEmpty()] 127 | [string[]]$ComputerName 128 | ) 129 | 130 | if (-not $PSBoundParameters.ContainsKey('ComputerName')) { 131 | $null = cmdkey /delete:$TargetName 132 | } else { 133 | foreach ($c in $ComputerName) { 134 | $invParams = @{ 135 | ComputerName = $c 136 | Command = "cmdkey /delete:$TargetName" 137 | } 138 | $null = Invoke-PsExec @invParams 139 | } 140 | } 141 | 142 | } 143 | 144 | function New-CachedCredential 145 | { 146 | [OutputType([void])] 147 | [CmdletBinding()] 148 | param 149 | ( 150 | [Parameter(Mandatory)] 151 | [ValidateNotNullOrEmpty()] 152 | [string]$TargetName, 153 | 154 | [Parameter()] 155 | [ValidateNotNullOrEmpty()] 156 | [pscredential]$Credential, 157 | 158 | [Parameter()] 159 | [ValidateNotNullOrEmpty()] 160 | [string[]]$ComputerName 161 | ) 162 | 163 | if (-not $PSBoundParameters.ContainsKey('ComputerName')) { 164 | $null = cmdkey /add:$TargetName /user:$Credential.UserName /pass:($Credential.GetNetworkCredential().Password) 165 | } else { 166 | foreach ($c in $ComputerName) { 167 | $invParams = @{ 168 | ComputerName = $c 169 | Command = "cmdkey /add:$TargetName /user:$($Credential.UserName) /pass:$($Credential.GetNetworkCredential().Password)" 170 | } 171 | $null = Invoke-PsExec @invParams 172 | } 173 | } 174 | 175 | } 176 | 177 | function ConvertTo-MatchValue 178 | { 179 | [OutputType('string')] 180 | [CmdletBinding()] 181 | param 182 | ( 183 | [Parameter(Mandatory)] 184 | [ValidateNotNullOrEmpty()] 185 | [string]$String, 186 | 187 | [Parameter(Mandatory)] 188 | [ValidateNotNullOrEmpty()] 189 | [string]$RegularExpression 190 | ) 191 | 192 | ([regex]::Match($String,$RegularExpression)).Groups[1].Value 193 | 194 | } 195 | 196 | function ConvertTo-CachedCredential 197 | { 198 | [OutputType('pscustomobject')] 199 | [CmdletBinding()] 200 | param 201 | ( 202 | [Parameter(Mandatory)] 203 | $CmdKeyOutput 204 | ) 205 | 206 | if (-not ($CmdKeyOutput.where({ $_ -match '\* NONE \*' }))) { 207 | if (@($CmdKeyOutput).Count -eq 1) { 208 | $CmdKeyOutput = $CmdKeyOutput -split "`n" 209 | } 210 | $nullsRemoved = $CmdKeyOutput.where({ $_ }) 211 | $i = 0 212 | foreach ($j in $nullsRemoved) { 213 | if ($j -match '^\s+Target:') { 214 | [pscustomobject]@{ 215 | Name = (ConvertTo-MatchValue -String $j -RegularExpression 'Target: .+:target=(.*)$').Trim() 216 | Category = (ConvertTo-MatchValue -String $j -RegularExpression 'Target: (.+):').Trim() 217 | Type = (ConvertTo-MatchValue -String $nullsRemoved[$i + 1] -RegularExpression 'Type: (.+)$').Trim() 218 | User = (ConvertTo-MatchValue -String $nullsRemoved[$i + 2] -RegularExpression 'User: (.+)$').Trim() 219 | Persistence = ($nullsRemoved[$i + 3]).Trim() 220 | } 221 | } 222 | $i++ 223 | } 224 | } 225 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PSCredentialManager 2 | 3 | PSCredentialManager is a PowerShell module that allows the user to manage cached credentials across the local, a remote or many remote computers at once. By it's nature, cached credentials are usually managed on local machines only. However, by using a combination of the great `psexec` tool and the `cmdkey` utility, a PowerShell module can be crafted around these tools to provide a seamless way to list, add and remove cached credentials from many computers at once! 4 | 5 | ## Example Usage 6 | 7 | ### Retrieving all credentials stored locally 8 | 9 | `PS> Get-CachedCredential` 10 | 11 | ## Retrieing all credentials stored on a remote computer 12 | 13 | `PS> Get-CachedCredential -ComputerName REMOTE` 14 | 15 | ## Retrieing a credential matching a certain name on a remote computer 16 | 17 | `PS> Get-CachedCredential -ComputerName REMOTE -TargetName FOO` 18 | 19 | ## Retrieving all credentials stored on many remote computers 20 | 21 | `PS> Get-CachedCredential -ComputerName REMOTE,REMOTE2,REMOTE3` 22 | 23 | ## Adding credentials 24 | 25 | `PS> New-CachedCreential -TargetName 'FOO' -UserName userhere -Password passhere` 26 | 27 | ## Removing credentials 28 | 29 | `PS> Remove-CachedCredential` 30 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | nuget_apikey: 3 | secure: Bvblx9A9AMTXl4LdQe4Aljoy1WBh48/U3UnlCSxDenF8X6tXEDLqzHdt3krnt7R6 4 | image: WMF 5 5 | install: 6 | - ps: .\buildscripts\install.ps1 7 | build_script: 8 | - ps: .\buildscripts\build.ps1 9 | test_script: 10 | - ps: .\buildscripts\test.ps1 11 | after_test: 12 | - ps: .\buildscripts\publish.ps1 13 | 14 | notifications: 15 | - provider: Slack 16 | incoming_webhook: 17 | secure: 0SXYOxoVQQeHXAdongVM4NBe7RMvYqowUywID9d1zpyIAM5Qa7cL9GeSFLc0mfGeSVVdL0c44XhEtei7PEHnsHdN057VRixyBqBRLBc5QAo= -------------------------------------------------------------------------------- /buildscripts/build.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = 'Stop' 2 | 3 | try { 4 | 5 | $manifestFilePath = "$env:APPVEYOR_BUILD_FOLDER\PSCredentialManager.psd1" 6 | $manifestContent = Get-Content -Path $manifestFilePath -Raw 7 | 8 | ## Update the module version based on the build version and limit exported functions 9 | $replacements = @{ 10 | "ModuleVersion = '.*'" = "ModuleVersion = '$env:APPVEYOR_BUILD_VERSION'" 11 | "FunctionsToExport = '\*'" = "FunctionsToExport = 'Get-CachedCredential','Removed-CachedCredential','New-CachedCredential'" 12 | } 13 | 14 | $replacements.GetEnumerator() | foreach { 15 | $manifestContent = $manifestContent -replace $_.Key, $_.Value 16 | } 17 | 18 | $manifestContent | Set-Content -Path $manifestFilePath 19 | 20 | } catch { 21 | Write-Error -Message $_.Exception.Message 22 | $host.SetShouldExit($LastExitCode) 23 | } -------------------------------------------------------------------------------- /buildscripts/install.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = 'Stop' 2 | 3 | try { 4 | 5 | $provParams = @{ 6 | Name = 'NuGet' 7 | MinimumVersion = '2.8.5.208' 8 | Force = $true 9 | } 10 | 11 | $null = Install-PackageProvider @provParams 12 | $null = Import-PackageProvider @provParams 13 | 14 | $requiredModules = @('Pester','PowerShellGet','PSScriptAnalyzer') 15 | foreach ($m in $requiredModules) { 16 | Write-Host "Installing [$($m)] module..." 17 | Install-Module -Name $m -Force -Confirm:$false 18 | Remove-Module -Name $m -Force -ErrorAction Ignore 19 | Import-Module -Name $m 20 | } 21 | 22 | } catch { 23 | Write-Error -Message $_.Exception.Message 24 | $host.SetShouldExit($LastExitCode) 25 | } -------------------------------------------------------------------------------- /buildscripts/publish.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = 'Stop' 2 | 3 | try { 4 | ## Don't upload the build scripts and appveyor.yml to PowerShell Gallery 5 | $tempmoduleFolderPath = "$env:Temp\PSCredentialManager" 6 | $null = mkdir $tempmoduleFolderPath 7 | 8 | ## Remove all of the files/folders to exclude out of the main folder 9 | $excludeFromPublish = @( 10 | 'PSCredentialManager\\buildscripts' 11 | 'PSCredentialManager\\appveyor\.yml' 12 | 'PSCredentialManager\\\.git' 13 | 'PSCredentialManager\\\.nuspec' 14 | 'PSCredentialManager\\README\.md' 15 | 16 | ) 17 | $exclude = $excludeFromPublish -join '|' 18 | Get-ChildItem -Path $env:APPVEYOR_BUILD_FOLDER -Recurse | where { $_.FullName -match $exclude } | Remove-Item -Force -Recurse 19 | 20 | ## Publish module to PowerShell Gallery 21 | $publishParams = @{ 22 | Path = $env:APPVEYOR_BUILD_FOLDER 23 | NuGetApiKey = $env:nuget_apikey 24 | Repository = 'PSGallery' 25 | Force = $true 26 | Confirm = $false 27 | } 28 | Publish-Module @publishParams 29 | 30 | } catch { 31 | Write-Error -Message $_.Exception.Message 32 | $host.SetShouldExit($LastExitCode) 33 | } -------------------------------------------------------------------------------- /buildscripts/test.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = 'Stop' 2 | 3 | try { 4 | 5 | Import-Module -Name Pester 6 | $ProjectRoot = $ENV:APPVEYOR_BUILD_FOLDER 7 | 8 | $testResultsFilePath = "$ProjectRoot\TestResults.xml" 9 | 10 | $invPesterParams = @{ 11 | Path = "$ProjectRoot\PSCredentialManager.Tests.ps1" 12 | OutputFormat = 'NUnitXml' 13 | OutputFile = $testResultsFilePath 14 | EnableExit = $true 15 | } 16 | Invoke-Pester @invPesterParams 17 | 18 | $Address = "https://ci.appveyor.com/api/testresults/nunit/$($env:APPVEYOR_JOB_ID)" 19 | (New-Object 'System.Net.WebClient').UploadFile( $Address, $testResultsFilePath ) 20 | 21 | } catch { 22 | Write-Error -Message $_.Exception.Message 23 | $host.SetShouldExit($LastExitCode) 24 | } --------------------------------------------------------------------------------