From 64af16cc083d6cb8d1cb4996ae43aa2608dd137b Mon Sep 17 00:00:00 2001 From: Marco Blessing Date: Thu, 21 Sep 2017 13:32:15 +0200 Subject: [PATCH] Publish Pre-release (#1) * adds basic module layout * fix module manifest encoding * fix callsign in appveyor helper * adds challenge file related functions * adds connection manager functions * adds Test-ChallengeFile * adds item related functions * adds store related functions * adds cSpell dictionary * adds CredentialStore related Pester tests * [WIP] test Pester file * fix typo * adds file dependencies * [WIP] fix pester tests * fix exception state * [WIP] add file dependencies * fix gitkeep filename * set constant debug module version string * adds Pester Tests for New-CredentialStoreItem * adds basic readme file * adds functions to export; adds meta data * adds vscode debug config * adds test for optional dependencies * [WIP] Implements optional dependency test * adds taskrunner definitions * adds CBH * add gitignore file * adds basic Build tasks * typo fixed * adds build folder to ignore list * adds Cisco and NetApp opt dependencies * adds build task * fix end of line dequence * remove task.json error * adds sources for optional modules * enables Pester and posh-git * prepare pre-release --- .gitignore | 3 + .vscode/cSpell.json | 40 ++++ .vscode/launch.json | 48 ++++ .vscode/settings.json | 50 ++++ .vscode/tasks.json | 56 +++++ README.md | 85 ++++++- appveyor.yml | 56 +++++ bin/.gitkeep | 1 + resources/.gitkeep | 0 resources/cs/Broken_CS.json | 3 + resources/cs/Challenge.bin | 1 + resources/cs/CredentialStore.json | Bin 0 -> 2084 bytes src/ChallengeFile/Get-ChallengeFile.ps1 | 44 ++++ src/ChallengeFile/Set-ChallengeFile.ps1 | 72 ++++++ src/ChallengeFile/Test-ChallengeFile.ps1 | 48 ++++ src/Connection/Connect-To.ps1 | 213 ++++++++++++++++++ src/Connection/Disconnect-From.ps1 | 147 ++++++++++++ src/Dependency.json | 31 +++ src/Helper/Get-RandomKey.ps1 | 47 ++++ src/Helper/Resolve-Dependency.ps1 | 85 +++++++ src/Helper/Test-Module.ps1 | 104 +++++++++ src/Item/Get-CredentialStoreItem.ps1 | 109 +++++++++ src/Item/New-CredentialStoreItem.ps1 | 135 +++++++++++ src/Item/Remove-CredentialStoreItem.ps1 | 94 ++++++++ src/Item/Set-CredentialStoreItem.ps1 | 114 ++++++++++ src/Item/Test-CredentialStoreItem.ps1 | 93 ++++++++ src/PSCredentialStore.psd1 | 140 ++++++++++++ src/PSCredentialStore.psm1 | 10 + src/Store/Get-CredentialStore.ps1 | 69 ++++++ src/Store/New-CredentialStore.ps1 | 111 +++++++++ src/Store/Test-CredentialStore.ps1 | 61 +++++ tests/.gitkeep | 0 .../Item/01_New-CredentialStoreItem.Tests.ps1 | 65 ++++++ tests/Store/00_Get-CredentialStore.Tests.ps1 | 37 +++ tests/Store/00_New-CredentialStore.Tests.ps1 | 114 ++++++++++ tests/Store/00_Test-CredentialStore.Tests.ps1 | 40 ++++ tools/AppVeyor.psm1 | 178 +++++++++++++++ 37 files changed, 2503 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 .vscode/cSpell.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 appveyor.yml create mode 100644 bin/.gitkeep create mode 100644 resources/.gitkeep create mode 100644 resources/cs/Broken_CS.json create mode 100644 resources/cs/Challenge.bin create mode 100644 resources/cs/CredentialStore.json create mode 100644 src/ChallengeFile/Get-ChallengeFile.ps1 create mode 100644 src/ChallengeFile/Set-ChallengeFile.ps1 create mode 100644 src/ChallengeFile/Test-ChallengeFile.ps1 create mode 100644 src/Connection/Connect-To.ps1 create mode 100644 src/Connection/Disconnect-From.ps1 create mode 100644 src/Dependency.json create mode 100644 src/Helper/Get-RandomKey.ps1 create mode 100644 src/Helper/Resolve-Dependency.ps1 create mode 100644 src/Helper/Test-Module.ps1 create mode 100644 src/Item/Get-CredentialStoreItem.ps1 create mode 100644 src/Item/New-CredentialStoreItem.ps1 create mode 100644 src/Item/Remove-CredentialStoreItem.ps1 create mode 100644 src/Item/Set-CredentialStoreItem.ps1 create mode 100644 src/Item/Test-CredentialStoreItem.ps1 create mode 100644 src/PSCredentialStore.psd1 create mode 100644 src/PSCredentialStore.psm1 create mode 100644 src/Store/Get-CredentialStore.ps1 create mode 100644 src/Store/New-CredentialStore.ps1 create mode 100644 src/Store/Test-CredentialStore.ps1 create mode 100644 tests/.gitkeep create mode 100644 tests/Item/01_New-CredentialStoreItem.Tests.ps1 create mode 100644 tests/Store/00_Get-CredentialStore.Tests.ps1 create mode 100644 tests/Store/00_New-CredentialStore.Tests.ps1 create mode 100644 tests/Store/00_Test-CredentialStore.Tests.ps1 create mode 100644 tools/AppVeyor.psm1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..45b36c1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Don't local track test builds +bin/PSCredentialStore.zip +bin/PSCredentialStore/* diff --git a/.vscode/cSpell.json b/.vscode/cSpell.json new file mode 100644 index 0000000..40cc042 --- /dev/null +++ b/.vscode/cSpell.json @@ -0,0 +1,40 @@ +// cSpell Settings +{ + // Version of the setting file. Always 0.1 + "version": "0.1", + // language - current active spelling language + "language": "en", + // words - list of words to be always considered correct + "words": [ + "Cmdlet", + "Cmdlets", + "GUID", + "Hashtable", + "Httpclient", + "Multipart", + "NTFS", + "Params", + "Ponduit", + "Repo", + "Veyor", + "appveyor", + "callsign", + "choco", + "chocolatey", + "codecoverage", + "creds", + "formdata", + "googlemail", + "notlike", + "notmatch", + "powershellgallery", + "testresults", + "wildcards" + ], + // flagWords - list of words to be always considered incorrect + // This is useful for offensive words and common spelling errors. + // For example "hte" should be "the" + "flagWords": [ + "hte" + ] +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..a89c67e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,48 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "PowerShell", + "request": "launch", + "name": "PowerShell Launch Current File", + "script": "${file}", + "args": [], + "cwd": "${file}" + }, + { + "type": "PowerShell", + "request": "launch", + "name": "PowerShell Launch Current File in Temporary Console", + "script": "${file}", + "args": [], + "cwd": "${file}", + "createTemporaryIntegratedConsole": true + }, + { + "type": "PowerShell", + "request": "launch", + "name": "PowerShell Launch Current File w/Args Prompt", + "script": "${file}", + "args": [ + "${command:SpecifyScriptArgs}" + ], + "cwd": "${file}" + }, + { + "type": "PowerShell", + "request": "attach", + "name": "PowerShell Attach to Host Process", + "processId": "${command:PickPSHostProcess}", + "runspaceId": 1 + }, + { + "type": "PowerShell", + "request": "launch", + "name": "PowerShell Interactive Session", + "cwd": "${workspaceRoot}" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..05e9a14 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,50 @@ +// Place your settings in this file to overwrite default and user settings. +{ + // Set basic file related options: + "files.encoding": "utf8", + "files.eol": "\r\n", + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true, + // Formation and editor options + "editor.renderWhitespace": "boundary", + "editor.formatOnSave": true, + "editor.formatOnType": true, + "editor.rulers": [ + 116 + ], + // powershell general + "powershell.startAutomatically": true, + "powershell.useX86Host": false, + "powershell.enableProfileLoading": true, + "powershell.scriptAnalysis.enable": true, + // powershell code Formatting + "powershell.codeFormatting.openBraceOnSameLine": true, + "powershell.codeFormatting.newLineAfterOpenBrace": true, + "powershell.codeFormatting.newLineAfterCloseBrace": true, + "powershell.codeFormatting.whitespaceBeforeOpenBrace": true, + "powershell.codeFormatting.whitespaceBeforeOpenParen": true, + "powershell.codeFormatting.whitespaceAroundOperator": true, + "powershell.codeFormatting.whitespaceAfterSeparator": true, + "powershell.codeFormatting.ignoreOneLineBlock": true, + "powershell.codeFormatting.alignPropertyValuePairs": false, + // cspell spellchecker options + "cSpell.enabledLanguageIds": [ + "c", + "cpp", + "csharp", + "go", + "javascript", + "javascriptreact", + "json", + "latex", + "markdown", + "php", + "plaintext", + "powershell", + "python", + "text", + "typescript", + "typescriptreact", + "yml" + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..27bf957 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,56 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + // Start PowerShell + "windows": { + "command": "${env:windir}/System32/WindowsPowerShell/v1.0/powershell.exe", + "args": [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass" + ] + }, + "linux": { + "command": "/usr/bin/powershell", + "args": [ + "-NoProfile" + ] + }, + "osx": { + "command": "/usr/local/bin/powershell", + "args": [ + "-NoProfile" + ] + }, + "tasks": [ + { + "taskName": "Test", + "suppressTaskName": true, + "args": [ + "Write-Host 'Invoking Pester...'; $ProgressPreference = 'SilentlyContinue'; Invoke-Pester -Script '.\\tests\\*' -EnableExit $flase -PesterOption @{IncludeVSCodeMarker=$true};", + "Invoke-Command { Write-Host 'Completed Test task in task runner.' }" + ], + "problemMatcher": "$pester", + "group": { + "kind": "test", + "isDefault": true + } + }, + { + "taskName": "DebugBuild", + "suppressTaskName": true, + "args": [ + "Write-Host 'Invoking Build...';", + "Write-Host -Object 'Test previous builds.' -ForegroundColor Blue;", + "If (Test-Path -Path '.\\bin\\PSCredentialStore.zip') { Remove-Item -Path '.\\bin\\PSCredentialStore.zip' -Verbose};", + "Copy-Item -Path '.\\src\\' -Destination '.\\bin\\PSCredentialStore' -Recurse -Verbose -Force;", + "Compress-Archive -Path '.\\src\\*' -DestinationPath '.\\bin\\PSCredentialStore.zip' -Update -Verbose;" + ], + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} diff --git a/README.md b/README.md index 6bb1a66..03cfa8c 100644 --- a/README.md +++ b/README.md @@ -1 +1,84 @@ -# PSCredentialStore \ No newline at end of file +| AppVeyor Overall | AppVeyor Master | AppVeyor Dev | Coveralls.io | Download | +| :--------------: | :-------------: | :----------: | :-----------: | :--------:| +| [![Build status](https://ci.appveyor.com/api/projects/status/b4t8x88xai3ee7gk?svg=true)](https://ci.appveyor.com/project/OCram85/PSCredentialStore) | [![Build status](https://ci.appveyor.com/api/projects/status/b4t8x88xai3ee7gk/branch/master?svg=true)](https://ci.appveyor.com/project/OCram85/PSCredentialStore/branch/master) | [![Build status](https://ci.appveyor.com/api/projects/status/b4t8x88xai3ee7gk/branch/dev?svg=true)](https://ci.appveyor.com/project/OCram85/PSCredentialStore/branch/dev) | [![Coverage Status](https://coveralls.io/repos/github/OCram85/PSCredentialStore/badge.svg?branch=master)](https://coveralls.io/github/OCram85/PSCredentialStore?branch=master) | [![Download](https://img.shields.io/badge/powershellgallery-PSCredentialStore-blue.svg)](https://www.powershellgallery.com/packages/PSCredentialStore) + +General +======= + +The PSCredentialStore is an simple credential manager for PSCredentials. It stores multiple credential objects in a +simple json file. You can choose between a private and shared store. The private one exists in your profile and can +ony accessed by your account on the same machine. The shared store enables you to use different credentials for your +script without exposing them as plain text. + +**The shared store isn't 100% secure and I don't recommend using it in production!** + +PSCredentialStore was developed to simplify the delegation of complex powershell scripts. In this case you often +need to store credentials for non interactive usage like in scheduled tasks. + +To get started read the [about_PSCredentialStore](/src/en-US/about_PSCredential.help.txt) page. + +Installation +============ + +PowerShellGallery.com (Recommended Way) +--------------------------------------- + +* Make sure you use PowerShell 4.0 or higher with `$PSVersionTable`. +* Use the builtin PackageManagement and install with: `Install-Module PSCredentialStore` +* Done. Start exploring the Module with `Import-Module PSCredentialStore ; Get-Command -Module PSCredentialStore` + +Manual Way +---------- + +* Take a look at the [Latest Release](https://github.com/OCram85/PSCredentialStore/releases/latest) page. +* Download the `PSCredentialStore.zip`. +* Unpack the Zip and put it in your Powershell Module path. + * Don't forget to change the NTFS permission flag in the context menu. +* Start with `Import-Module PSCredentialStore` + +Quick Start +----------- + +**1.** First we need a blank CredentialStore. You can decide between a *private* or *shared* store. The private +Credential Store can only be accessed with your profile on the machine you created it. +```powershell +# Private Credential Store +New-CredentialStore + +# Shared Credential Store +New-CredentialStore -Shared + +#Shared CredentialStore in custom Location +New-CredentialStore -Shared -Path 'C:\CredentialStore.json' +``` + +**2.** Now you can manage your CredentialStoreItems: +```powershell +# This will prompt for credentials and stores it in a private store +New-CredentialStoreItem -RemoteHost 'dc01.myside.local' -Identifier 'AD' + +# You can now use it in other scripts like this: +$DCCreds = Get-CredentialStoreItem -RemoteHost 'dc01.myside.local' -Identifier 'AD' +Invoke-Command -ComputerName 'dc01.myside.local' -Credential $DCCreds -ScripBlock {Get-Process} +``` + +The CredentialStore contains also a simple function to establish a connection with several systems or protocols. +If you have already installed the underlying framework your can connect to: + +* **CiscoUcs** - Establish a connection to a Cisco UCS fabric interconnect. + * Required Modules: [`Cisco.UCS.Core`, `Cisco.UCSManager`](https://software.cisco.com/download/release.html?i=!y&mdfid=286305108&softwareid=284574017&release=2.1.1) +* **FTP** - Establish a connection to a FTP host. + * Required Modules: [`WinSCP`](https://www.powershellgallery.com/packages/WinSCP) +* **NetAppFAS** - Establish a connection to a NetApp Clustered ONTAP filer. + * Required Modules: [`DataONTAP`](http://mysupport.netapp.com/tools/info/ECMLP2310788I.html?productID=61926) +* **VMware** - Establish a connection to a VMware vCenter or ESXi host. + * Required Modules: [`VMware.VimAutomation.Core`](https://www.powershellgallery.com/packages/VMware.PowerCLI) + +Here are some basic examples: + +```powershell +Connect-To -RemoteHost "ucs.myside.local" -Type CiscoUcs +Connect-To -RemoteHost "ftp.myside.local" -Type FTP +Connect-To -RemoteHost "fas.myside.local" -Type NetAppFAS +Connect-To -RemoteHost "esx01.myside.local" -Type VMware +``` diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..22f5226 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,56 @@ +version: 0.1.{build} + +branches: + only: + - master + - dev + - debug + +skip_tags: true + +#image: WMF 5 +# Test ne build image: +image: Visual Studio 2017 + +# Install pester module and init the Appveyor support. +install: + - ps: Install-PackageProvider -Name NuGet -MinimumVersion '2.8.5.201' -Force -Verbose + - ps: Import-PackageProvider NuGet -MinimumVersion '2.8.5.201' -Force + - ps: Install-Module -Name 'Pester' -Scope CurrentUser -Force -SkipPublisherCheck -AllowClobber + - ps: Update-Module 'Pester' + - ps: Install-Module -Name 'posh-git' -Scope CurrentUser -Force -SkipPublisherCheck -AllowClobber + - ps: Update-Module 'posh-git' + - ps: Install-Module -Name 'PSCoverage' -Scope CurrentUser -Force -SkipPublisherCheck -AllowClobber + - ps: Import-Module 'PSCoverage' + - ps: Import-Module .\tools\AppVeyor.psm1 + +environment: + NuGetToken: + secure: 835qfZIkC9mE7QhkYfOZVAdR8rZhPvxG8BO4CbeaelRQOhlqmaSr8G1DWRJzZ/bS + CoverallsToken: + secure: eTjWqHL48MBr8wp1rSgLrXHdtpfDV/uClacP3svlWJfCvn/zVokpuaMnWM5RoyIY + +build: false + +before_build: + - ps: Invoke-AppVeyorBumpVersion + +build_script: + - ps: Invoke-AppVeyorBuild + +test_script: + - ps: Invoke-AppVeyorTests + - ps: Invoke-CoverageReport + +deploy: + - provider: GitHub + auth_token: + secure: M+bBX5/nKdJB0eViP7xtrLVTwf3vGDUA9N2MMprZp2i+9ZR3CBVcJnSzJWUmalhB + artifact: PSCredentialStore.zip # upload all NuGet packages to release assets + draft: true + prerelease: true + on: + branch: master # release from master branch only + +after_deploy: + - ps: Invoke-AppVeyorPSGallery diff --git a/bin/.gitkeep b/bin/.gitkeep new file mode 100644 index 0000000..7756187 --- /dev/null +++ b/bin/.gitkeep @@ -0,0 +1 @@ +This is a placeholder file. The build Server will create all bin files here. diff --git a/resources/.gitkeep b/resources/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/resources/cs/Broken_CS.json b/resources/cs/Broken_CS.json new file mode 100644 index 0000000..23e8eee --- /dev/null +++ b/resources/cs/Broken_CS.json @@ -0,0 +1,3 @@ +{ + "Version": "1.2.0", + "Creation": "2016-06-14 08:41:10" diff --git a/resources/cs/Challenge.bin b/resources/cs/Challenge.bin new file mode 100644 index 0000000..3b80551 --- /dev/null +++ b/resources/cs/Challenge.bin @@ -0,0 +1 @@ +!pH4"=wS2 \ No newline at end of file diff --git a/resources/cs/CredentialStore.json b/resources/cs/CredentialStore.json new file mode 100644 index 0000000000000000000000000000000000000000..4d45b386150656d9a9c66102720bfe29dfa7623e GIT binary patch literal 2084 zcmeH{%Wl*_5JhW^#6Lt{1Mze0$(A3;0@&MaTgrx@nFti2{5o)MbtcnxX2B4aQOW(N zuGek%t>3?X+FN^L7mt$J7n^Knd%Lk)`)Zk8`AKGleef?w_ip9$5s%4wyL;wUT8`Jo z-ZN_~tR`M=9dT=RSA3@;ezJS}M$BXWYjWJ!ch4gIx`vs0Z*Aaw_vE$2cEEUg&g?U& zh5eW_#$4w*?SIf_c1;a?(-W?vUbfHbqu{vy+OS_alU4)_pk2LvI=M<0iCoXtJrHAPSa2%#7+^>ImYp_TrEFgjlEk(XdjlQFp93U9wiy$!5e;cp|rix_kz1TcC yL$_GTx?>a*#pkWZi(XpS=o + + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [string]$Path = "{0}\PSCredentialStore\Challenge.bin" -f $env:ProgramData + ) + + if (Test-Path $Path) { + try { + [io.file]::ReadAllBytes($Path) + } + catch { + Write-Error ("Could not read file {0}." -f $Path) -ErrorAction Stop + } + } +} diff --git a/src/ChallengeFile/Set-ChallengeFile.ps1 b/src/ChallengeFile/Set-ChallengeFile.ps1 new file mode 100644 index 0000000..926c3f5 --- /dev/null +++ b/src/ChallengeFile/Set-ChallengeFile.ps1 @@ -0,0 +1,72 @@ +Function Set-ChallengeFile() { + <# + .SYNOPSIS + Writes the given key into the challenge file + + .DESCRIPTION + You can use the file content for ConvertTo-SecureString operations. + + .PARAMETER Path + The file you wish to create. + + .PARAMETER KeySize + Specify the key size for the encryption key. + + .PARAMETER Force + Use this switch to override an older file version. + + .INPUTS + [None] + + .OUTPUTS + [None] + + .EXAMPLE + .\Set-ChallengeFile -Path "C:\TMP\myfile.json" -Force + + .NOTES + File Name : Set-ChallengeFile.ps1 + Author : Marco Blessing - marco.blessing@googlemail.com + Requires : + + .LINK + https://github.com/OCram85/PSCredentialStore + #> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [string]$Path = "{0}\PSCredentialStore\Challenge.bin" -f $env:ProgramData, + + [Parameter(Mandatory = $false)] + [ValidateSet(16, 24, 32)] + [string]$KeySize = "24", + + [switch]$Force + ) + + if ((Test-Path -Path $Path)) { + if ($Force -eq $true) { + Remove-Item -Path $Path -Confirm:$false -Force + } + else { + Write-Error "The given file already exists!. Use the -Force switch to override it." -ErrorAction Stop + } + } + $PSCredentialStoreDataDir = Split-Path -Path $Path -Parent + if (-not (Test-Path $PSCredentialStoreDataDir)) { + try { + New-Item -ItemType Directory -Path $PSCredentialStoreDataDir + } + catch { + Write-Error ("Could not create the parent data dir {0}" -f $PSCredentialDataDir) -ErrorAction Stop + } + } + try { + $Keys = Get-RandomKey -Size $KeySize + [io.file]::WriteAllBytes($Path, $Keys) + } + catch { + $_.Exception | Format-List -Force | Out-String | Write-Error -ErrorAction Stop + } +} diff --git a/src/ChallengeFile/Test-ChallengeFile.ps1 b/src/ChallengeFile/Test-ChallengeFile.ps1 new file mode 100644 index 0000000..77eaa06 --- /dev/null +++ b/src/ChallengeFile/Test-ChallengeFile.ps1 @@ -0,0 +1,48 @@ +function Test-ChallengeFile { + <# + .SYNOPSIS + Simple path check for challenge file needed by the CredentialStores. + + .DESCRIPTION + This is supposed to be a internal function to check the existence for a challenge file. + + .PARAMETER Path + Specify the path to the challenge file. + + .INPUTS + [None] + + .OUTPUTS + [Bool]. + + .EXAMPLE + If (Test-ChallengeFile) { + Write-Host "The file exists." + } + Else { + Write-Warning "Couldn't find the given file!" + } + + .NOTES + File Name : Test-ChallengeFile.ps1 + Author : Marco Blessing - marco.blessing@googlemail.com + Requires : + + .LINK + https://github.com/OCram85/PSCredentialStore + #> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [String]$Path = "{0}\PSCredentialStore\Challenge.bin" -f $env:ProgramData + ) + + if (Test-Path $Path) { + $true + } + else { + $false + } +} diff --git a/src/Connection/Connect-To.ps1 b/src/Connection/Connect-To.ps1 new file mode 100644 index 0000000..740ee6a --- /dev/null +++ b/src/Connection/Connect-To.ps1 @@ -0,0 +1,213 @@ +function Connect-To { + <# + .SYNOPSIS + Connects to the given host using the stored CredentialStoreItem. + + .DESCRIPTION + Establish a connection to the selected host using a stored CredentialStoreItem. + + .PARAMETER RemoteHost + Specify the host, for which you would like to change the credentials. + + .PARAMETER Identifier + Defaults to "". Specify a string, which separates two CredentialStoreItems for the + same hostname. + + .PARAMETER Type + Specify the host type of the target. Currently implemented targets are: + - CiscoUcs Establish a connection to a Cisco UCS fabric interconnect. + - FTP Establish a connection to a FTP host. + - NetAppFAS Establish a connection to a NetApp Clustered ONTAP filer. + - VMware Establish a connection to a VMware vCenter or ESXi host. + + .PARAMETER Credentials + Use this parameter to bypass the stored credentials. Without this parameter Connect-To tries to read the + needed credentials from the CredentialStore. If you provide this parameter you skip this lookup behavior. + So you can use it to enable credentials without preparing any user interaction. + + .PARAMETER Path + Define a custom path to a shared CredentialStore. + + .PARAMETER Shared + Switch to shared mode with this param. This enforces the command to work with a shared CredentialStore which + can be decrypted across systems. + + .INPUTS + [None] + + .OUTPUTS + [None] + + .EXAMPLE + Connect-To -RemoteHost "ucs.myside.local" -Type CiscoUcs + Connect-To -RemoteHost "ftp.myside.local" -Type FTP + Connect-To -RemoteHost "fas.myside.local" -Type NetAppFAS + Connect-To -RemoteHost "esx01.myside.local" -Type VMware + + .EXAMPLE + $MyCreds = Get-Credential + Connect-To -RemoteHost "vcr01.myside.local" -Type VMware -Credentials $MyCreds + Get-VM -Name "*vlm*" | Select-Object -Property Name + Disconnect-From -RemoteHost "vcr01.myside.local" -Type VMware + + .NOTES + File Name : Connect-To.ps1 + Author : Marco Blessing - marco.blessing@googlemail.com + Requires : PSFTP, PowerCLI + + .LINK + https://github.com/OCram85/PSCredentialStore + #> + [CmdletBinding(DefaultParameterSetName = "Private")] + param( + [Parameter(Mandatory = $true, ParameterSetName = "Shared")] + [Parameter(Mandatory = $true, ParameterSetName = "Private")] + [String]$RemoteHost, + + [Parameter(Mandatory = $false, ParameterSetName = "Shared")] + [Parameter(Mandatory = $false, ParameterSetName = "Private")] + [String]$Identifier, + + [Parameter(Mandatory = $true, ParameterSetName = "Shared")] + [Parameter(Mandatory = $true, ParameterSetName = "Private")] + [ValidateSet("CiscoUcs", "FTP", "NetAppFAS", "VMware")] + [String]$Type, + + [Parameter(Mandatory = $False, ParameterSetName = "Shared")] + [Parameter(Mandatory = $False, ParameterSetName = "Private")] + [PSCredential]$Credentials, + + [Parameter(Mandatory = $False, ParameterSetName = "Shared")] + [ValidateNotNullOrEmpty()] + [String]$Path = "{0}\PSCredentialStore\CredentialStore.json" -f $env:ProgramData, + + [Parameter(Mandatory = $false, ParameterSetNAme = "Shared")] + [Switch]$Shared + ) + + begin { + # First check the optional modules + if (-not (Resolve-Dependency -Name $Type)) { + Write-Error -Message ("Could not resolve the optional dependencies defined for {0}" -f $Type) -ErrorAction 'Stop' + } + switch ($Type) { + "VMware" { + # Disable the yellow certificate warning, since we haven't replaced the SSL certs for vCenter/ESXi + $null = Set-PowerCLIConfiguration -Scope Session -InvalidCertificateAction Ignore -Confirm:$false + + # Disable connecting through proxy, since vCenter isn't somewhere we need a proxy for. + $null = Set-PowerCLIConfiguration -Scope Session -ProxyPolicy NoProxy -Confirm:$false + } + } + } + + process { + # Set the correct CredentialStore Path depending on the used ParameterSetName + if ($PSCmdlet.ParameterSetName -eq "Private") { + $Path = "{0}\CredentialStore.json" -f $env:APPDATA + } + if (-not ($Credentials)) { + # Load the credential from the CredentialStore. If the credential doesn't exist, we need to + # return 1, so a calling if statement can handle the failure detection. + + # Check if $Identifier has been defined, in which case we need to use different name for + # the lookup of the CredentialStoreItem. + try { + if ($Identifier -ne "") { + $RemoteHostIdentifier = "{0}/{1}" -f $Identifier, $RemoteHost + $creds = Get-CredentialStoreItem -RemoteHost $RemoteHostIdentifier -Path $Path + } + else { + $creds = Get-CredentialStoreItem -RemoteHost $RemoteHost -Path $Path + } + } + + catch { + Write-Message2 ("Unable to look up credential store item for RemoteHost {0}/Identifier {1}!" -f $RemoteHost, $Identifier) -ErrorAction Stop + } + } + else { + $creds = $Credentials + } + + if ($creds.UserName -eq "" -or $creds.Password.GetType().Name -ne "SecureString") { + # Write a error message to the log. + Write-Message2 ("Please provide valid credentials for RemoteHost {0}!" -f $RemoteHost) -ErrorAction Stop + } + else { + switch ($Type) { + "CiscoUcs" { + try { + $handle = Connect-Ucs -Name $RemoteHost -Credential $creds -ErrorAction Stop + $ExecutionContext.SessionState.PSVariable.Set("DefaultUcs", $handle) + } + + catch { + # Write a error message to the log. + Write-Message2 ("Unable to connect to {0} using Type {1}." -f $RemoteHost, $Type) -ErrorAction Stop + } + } + "FTP" { + # First establish the FTP session + $WinSCPConParams = @{ + Credential = $creds + Hostname = $RemoteHost + Protocol = 'Ftp' + FtpMode = 'Passive' + } + try { + $Global:WinSCPSession = New-WinSCPSession @WinSCPConParams + } + catch { + throw "Could not connect to {0} using {1} protocol!" -f $RemoteHost, $Type + } + # Check the Connection State + if (!($WinSCPSession.Opened)) { + # Check the connection state and find out if the session is still open. + $MessageParams = @{ + Message = "Connection to {0} using Type {1} was established. But now it seems to be lost!" -f $RemoteHost, $Type + ErrorAction = "Stop" + } + Write-Error @MessageParams + } + } + "NetAppFAS" { + try { + $null = Connect-NcController -Name $RemoteHost -Credential $creds -ErrorAction Stop -HTTPS + } + + catch { + # Write a error message to the log. + $MessageParams = @{ + Message = "Unable to connect to {0} using Type {1}." -f $RemoteHost, $Type + ErrorAction = "Stop" + } + Write-Error @MessageParams + } + } + "VMware" { + try { + Connect-VIServer -Server $RemoteHost -Credential $creds -ErrorAction Stop | Out-Null + } + + catch { + # Write a error message to the log. + $MessageParams = @{ + Message = "Unable to connect to {0} using Type {1}." -f $RemoteHost, $Type + ErrorAction = "Stop" + } + Write-Error @MessageParams + } + } + default { + # Write a error message to the log. + $MessageParams = @{ + Message = "Unable to connect to {0} using Type {1}." -f $RemoteHost, $Type + ErrorAction = "Stop" + } + Write-Error @MessageParams + } + } + } + } +} diff --git a/src/Connection/Disconnect-From.ps1 b/src/Connection/Disconnect-From.ps1 new file mode 100644 index 0000000..8fa8c2b --- /dev/null +++ b/src/Connection/Disconnect-From.ps1 @@ -0,0 +1,147 @@ +function Disconnect-From { + <# + .SYNOPSIS + Terminates a session established with Connect-To using a CredentialStoreItem. + + .DESCRIPTION + Terminates a session established with Connect-To using a CredentialStoreItem. + + .PARAMETER RemoteHost + Specify the remote endpoint, whose session you would like to terminate. + + .PARAMETER Identifier + Defaults to "". Specify a string, which separates two CredentialStoreItems for the + same hostname. + + .PARAMETER Type + Specify the host type of the target. Currently implemented targets are: + - CiscoUcs Establish a connection to a Cisco UCS Fabric Interconnect. + - FTP Establish a connection to a FTP host. + - NetAppFAS Establish a connection to a NetApp Clustered ONTAP filer. + - VMware Establish a connection to a VMware vCenter or ESXi host. + + .PARAMETER Force + Force the disconnect, even if the disconnect would fail. + + .INPUTS + [None] + + .OUTPUTS + [None] + + .EXAMPLE + Disconnect-From -RemoteHost "ucs.myside.local" -Type CiscoUcs + + .EXAMPLE + Disconnect-From -RemoteHost "ftp.myside.local" -Type FTP + + .EXAMPLE + Disconnect-From -RemoteHost "fas.myside.local" -Type NetAppFAS + + .EXAMPLE + Disconnect-From -RemoteHost "esx01.myside.local" -Type VMware + + .EXAMPLE + Disconnect-From -RemoteHost "esx01.myside.local" -Type VMware -Force:$True + + .NOTES + File Name : Disconnect-To.ps1 + Author : Marco Blessing - marco.blessing@googlemail.com + Requires : + + .LINK + https://github.com/OCram85/PSCredentialStore + #> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$RemoteHost, + + [Parameter(Mandatory = $true)] + [ValidateSet("CiscoUcs", "FTP", "NetAppFAS", "VMware")] + [string]$Type, + + [Parameter(Mandatory = $false)] + [switch]$Force + ) + + switch ($Type) { + "VMware" { + try { + if ($Force) { + Disconnect-VIServer -Server $RemoteHost -Confirm:$false -ErrorAction Stop -Force:$true + } + else { + Disconnect-VIServer -Server $RemoteHost -Confirm:$false -ErrorAction Stop + } + } + + catch { + # Write a error message to the log. + $MessageParams = @{ + Message = "Unable to disconnect from {0} using Type {1}." -f $RemoteHost, $Type + ErrorAction = "Stop" + } + Write-Error @MessageParams + } + + } + # Check for an existing WinSCP Session var + "FTP" { + if ($Global:WinSCPSession.Opened) { + Remove-WinSCPSession -WinSCPSession $Global:WinSCPSession + } + else { + $MessageParams = @{ + Message = "There is no open WinSCP Session" + ErrorAction = "Stop" + } + Write-Error @MessageParams + } + } + # DataONTAP doesn't have a CmdLet `Disconnect-NcController`. + # So we go ahead and clear the CurrentNcController variable. + "NetAppFAS" { + try { + $MessageParams = @{ + Message = "Setting {0} to `$null, which will disconnect NetAppFAS" -f $Global:CurrentNcController + ErrorAction = "Continue" + } + Write-Verbose @MessageParams + $Global:CurrentNcController = $null + } + catch { + # Write a error message to the log. + $MessageParams = @{ + Message = "Unable to disconnect from {0} using Type {1}." -f $RemoteHost, $Type + ErrorAction = "Stop" + } + Write-Error @MessageParams + } + + } + "CiscoUcs" { + try { + Disconnect-Ucs -Ucs $RemoteHost + } + + catch { + # Write a error message to the log. + $MessageParams = @{ + Message = "Unable to disconnect from {0} using Type {1}." -f $RemoteHost, $Type + ErrorAction = "Stop" + } + Write-Error @MessageParams + } + } + default { + # Write a error message to the log. + $MessageParams = @{ + Message = "Unable to disconnect from {0} using Type {1}." -f $RemoteHost, $Type + ErrorAction = "Stop" + } + Write-Error @MessageParams + } + } +} diff --git a/src/Dependency.json b/src/Dependency.json new file mode 100644 index 0000000..d76a42f --- /dev/null +++ b/src/Dependency.json @@ -0,0 +1,31 @@ +{ + "Version": 0.1, + "Mandatory": {}, + "Optional": [ + { + "Name": "VMware", + "Modules": [ + "VMware.VimAutomation.Core" + ] + }, + { + "Name": "CiscoUCS", + "Modules": [ + "Cisco.UCS.Core", + "Cisco.UCSManager" + ] + }, + { + "Name": "FTP", + "Modules": [ + "WinSCP" + ] + }, + { + "Name": "NetAppFAS", + "Modules": [ + "DataONTAP" + ] + } + ] +} diff --git a/src/Helper/Get-RandomKey.ps1 b/src/Helper/Get-RandomKey.ps1 new file mode 100644 index 0000000..b1ea7e3 --- /dev/null +++ b/src/Helper/Get-RandomKey.ps1 @@ -0,0 +1,47 @@ +function Get-RandomKey { + <# + .SYNOPSIS + Returns a random key + + .DESCRIPTION + You can use the key for further use with SecureStrings. + + .PARAMETER Size + Define the key size. You can choose between 16, 24 and 32 + + .INPUTS + [None] + + .OUTPUTS + Returns a Random key as [Byte[]] array. + + .EXAMPLE + .\Get-RandomKey -Size 24 + + .NOTES + File Name : Get-RandomKey.ps1 + Author : Marco Blessing - marco.blessing@googlemail.com + Requires : + + .LINK + https://github.com/OCram85/PSCredentialStore + #> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [ValidateSet(16, 24, 32)] + [string]$size + ) + # Init the vars + [Byte[]]$Key = @() + $i = 0 + + while ($i -ne $size) { + $element = Get-Random -Minimum 0 -Maximum 255 + Write-Debug ("The current element is {0}." -f $element) + $Key += $element + $i++ + } + $Key +} diff --git a/src/Helper/Resolve-Dependency.ps1 b/src/Helper/Resolve-Dependency.ps1 new file mode 100644 index 0000000..9d76571 --- /dev/null +++ b/src/Helper/Resolve-Dependency.ps1 @@ -0,0 +1,85 @@ +function Resolve-Dependency { + <# + .SYNOPSIS + Tests defined optional dependencies and returns the result as bool. + + .DESCRIPTION + Use this function to test for optional modules. You can use it if you provide functions which needs special + modules but you don't want to make them required. + + Place a file called Dependency.json in your module root dir. The default format is: + + { + "Version": 0.1, + "Mandatory": {}, + "Optional": [ + { + "Name": "VMware", + "Modules": [ + "VMware.VimAutomation.Core" + ] + }, + { + "Name": "CiscoUCS", + "Modules": [] + } + ] + } + + .PARAMETER Name + Select the dependency item name you defined in the dependency.json. + .INPUTS + [None] + + .OUTPUTS + [bool] + + .EXAMPLE + If (-not (Resolve-Dependency -Name 'VMware')) { + Write-Error -Message ("Could not resolve the optional dependencies defined for {0}" -f 'VMware') -ErrorAction 'Stop' + } + + .NOTES + File Name : ResolveDependency.ps1 + Author : Marco Blessing - marco.blessing@googlemail.com + Requires : + + .LINK + https://github.com/OCram85/PSCredentialStore + #> + [OutputType([bool])] + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$Name + ) + + begin { + $ModuleRootDir = $MyInvocation.MyCommand.Module.ModuleBase + $DepFilePath = Join-Path -Path $ModuleRootDir -ChildPath "Dependency.json" + if (Test-Path -Path $DepFilePath) { + $Dependency = Get-Content -Path $DepFilePath -Raw -Encoding UTF8 | ConvertFrom-Json + } + else { + Write-Warning ("Could not find the dependency file: {0}" -f $DepFilePath) + } + $res = @() + } + + process { + $SelectedDependency = $Dependency.Optional | Where-Object {$_.Name -match $Name} + + foreach ($Module in $SelectedDependency.Modules) { + $res += Test-Module -Name $Module + } + if ($res -contains $false) { + return $false + } + else { + return $true + } + } + + end { + } +} diff --git a/src/Helper/Test-Module.ps1 b/src/Helper/Test-Module.ps1 new file mode 100644 index 0000000..7f18f95 --- /dev/null +++ b/src/Helper/Test-Module.ps1 @@ -0,0 +1,104 @@ +function Test-Module { + <# + .SYNOPSIS + Tests if the given module exists on the local system. + + .DESCRIPTION + Tests if the given module is installed on the local system. It returns a bool value as result. + + .PARAMETER Name + Define a item name you need to test + + .PARAMETER Type + Define the dependency type. This could be a Module or PSnapin. + + .PARAMETER MessagePattern + You an optionally adjust the message pattern for the error message itself. + The available placeholders are: + - {0} : Type + - {1} : Name + + .PARAMETER StopIfFails + This switch forces the entire script to stop if the given dependency object fails. + + .INPUTS + [None] + + .OUTPUTS + [Bool] + + .EXAMPLE + .\Test-Dependency -Name 'VMware.PowerCLI' -Type 'Module' + + .EXAMPLE + .\Test-Dependency -Name 'VMware.PowerCLI' -Type 'Module' -StopIfFails + + .NOTES + File Name : Get-RandomKey.ps1 + Author : Marco Blessing - marco.blessing@googlemail.com + Requires : + + .LINK + https://github.com/OCram85/PSCredentialStore + #> + [OutputType([bool])] + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$Name, + + [Parameter(Mandatory = $false)] + [ValidateSet('Module', 'PSSnapin', 'Custom')] + [string]$Type = 'Module', + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string]$MessagePattern = @" +Could not find the required {0} called {1}. Please install the required {0} to run this function! +"@, + [Parameter(Mandatory = $false)] + [switch]$StopIfFails + ) + begin { + + } + + process { + $Message = $MessagePattern -f $Type, $Name + Write-Debug $Message + switch ($Type) { + 'Module' { + if (Get-Module -Name $Name -ListAvailable) { + return $true + } + else { + if ($StopIfFails) { + Write-Error -Message $Message -ErrorAction Stop -Category NotInstalled + } + return $false + } + } + + 'PSSnapin' { + if (Get-PSSnapin -Name $Name -Registered) { + return $true + } + else { + if ($StopIfFails) { + Write-Error -Message $Message -ErrorAction Stop -Category NotInstalled + return $false + } + } + } + + 'Custom' { + Throw 'Custom tests are not implemented yet!' + } + } + } + + end { + + } +} diff --git a/src/Item/Get-CredentialStoreItem.ps1 b/src/Item/Get-CredentialStoreItem.ps1 new file mode 100644 index 0000000..4236c50 --- /dev/null +++ b/src/Item/Get-CredentialStoreItem.ps1 @@ -0,0 +1,109 @@ +function Get-CredentialStoreItem { + <# + .SYNOPSIS + Returns the Credential from a given remote host item. + + .DESCRIPTION + Return the credential as PSCredential object. + + + .PARAMETER RemoteHost + Specify the host, for which you would like to change the credentials. + + .PARAMETER Identifier + Provide a custom identifier to the given remote host key. This enables you to store multiple credentials + for a single remote host entry. For example ad/sys1, ftp/sys1, mssql/sys1 + + .PARAMETER Path + Define a custom path to a shared CredentialStore. + + .PARAMETER Shared + Switch to shared mode with this param. This enforces the command to work with a shared CredentialStore which + can be decrypted across systems. + + .INPUTS + [None] + + .OUTPUTS + [System.Management.Automation.PSCredential] + + .EXAMPLE + $myCreds = Get-CredentialStoreItem -Path "C:\TMP\mystore.json" -RemoteHost "esx01.myside.local" + + .NOTES + File Name : Get-CredentialStoreItem.ps1 + Author : Marco Blessing - marco.blessing@googlemail.com + Requires : + + .LINK + https://github.com/OCram85/PSCredentialStore + #> + + [CmdletBinding(DefaultParameterSetName = "Private")] + [OutputType([System.Management.Automation.PSCredential])] + param( + [Parameter(Mandatory = $false, ParameterSetName = "Shared")] + [string]$Path = "{0}\PSCredentialStore\CredentialStore.json" -f $env:ProgramData, + + [Parameter(Mandatory = $true, ParameterSetName = "Shared")] + [Parameter(Mandatory = $true, ParameterSetName = "Private")] + [ValidateNotNullOrEmpty()] + [string]$RemoteHost, + + [Parameter(Mandatory = $false, ParameterSetName = "Shared")] + [Parameter(Mandatory = $false, ParameterSetName = "Private")] + [ValidateNotNullOrEmpty()] + [string]$Identifier, + + [Parameter(Mandatory = $false, ParameterSetName = "Shared")] + [switch]$Shared + ) + + # First set a constand path for private CredentialStore mode. + if ($PSCmdlet.ParameterSetName -eq "Private") { + $Path = "{0}\CredentialStore.json" -f $env:APPDATA + } + + if ($Identifier -ne "") { + $CredentialName = $RemoteHost = "{0}/{1}" -f $Identifier, $RemoteHost + } + else { + $CredentialName = $RemoteHost + } + + if (Test-CredentialStore -Path $Path) { + $CS = Get-CredentialStore -Path $Path + $CSMembers = Get-Member -InputObject $CS + # Let`s first check if the given remote host exists as object property + if (($CSMembers.MemberType -eq "NoteProperty") -and ($CSMembers.Name -eq $CredentialName)) { + if ($CS.Type -eq "Private") { + $CSItem = [ordered]@{ + User = $CS.$CredentialName.User + Password = ConvertTo-SecureString -String $CS.$CredentialName.Password + } + } + else { + $Key = Get-ChallengeFile + $CSItem = [ordered]@{ + User = $CS.$CredentialName.User + Password = ConvertTo-SecureString -String $CS.$CredentialName.Password -Key $Key + } + } + New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $CSItem.User, $CSItem.Password + } + else { + $MsgParams = @{ + ErrorAction = "Stop" + Message = "Could not find credentials for the given remote host: {0}" -f $RemoteHost + } + Write-Error @MsgParams + } + } + else { + $MsgParams = @{ + ErrorAction = "Stop" + Message = "The given credential store ({0}) does not exist!" -f $Path + } + Write-Error @MsgParams + } +} diff --git a/src/Item/New-CredentialStoreItem.ps1 b/src/Item/New-CredentialStoreItem.ps1 new file mode 100644 index 0000000..b643659 --- /dev/null +++ b/src/Item/New-CredentialStoreItem.ps1 @@ -0,0 +1,135 @@ +function New-CredentialStoreItem { + <# + .SYNOPSIS + Adds a credential store item containing host, user and password to the given store. + + .DESCRIPTION + The credentials are stored without any relations to it's further use. If you need to change an existing + item please use Set-CredentialStoreItem. You need to decide afterwards, whether to use the credential for + a VIConnection, NetApp FAS or UCS Fabric Interconnect. + + .PARAMETER Path + Define the store in which you would like to add a new item. + + .PARAMETER RemoteHost + The identifier or rather name for the given credentials. + + .PARAMETER Identifier + Provide a custom identifier to the given remote host key. This enables you to store multiple credentials + for a single remote host entry. For example ad/sys1, ftp/sys1, mssql/sys1 + + .PARAMETER Credential + You can provide credentials optionally as pre existing pscredential object. + + .INPUTS + [None] + + .OUTPUTS + [None] + + .EXAMPLE + New-CredentialStoreItem -Path "C:\TMP\mystore.json" -RemoteHost "esx01.myside.local" + + .NOTES + File Name : New-CredentialStoreItem.ps1 + Author : Marco Blessing - marco.blessing@googlemail.com + Requires : + + .LINK + https://github.com/OCram85/PSCredentialStore + #> + + [CmdletBinding(DefaultParameterSetName = "Private")] + param( + [Parameter(Mandatory = $false, ParameterSetName = "Shared")] + [string]$Path = "{0}\PSCredentialStore\CredentialStore.json" -f $env:ProgramData, + + [Parameter(Mandatory = $true, ParameterSetName = "Shared")] + [Parameter(Mandatory = $true, ParameterSetName = "Private")] + [ValidateNotNullOrEmpty()] + [string]$RemoteHost, + + [Parameter(Mandatory = $false, ParameterSetName = "Shared")] + [Parameter(Mandatory = $false, ParameterSetName = "Private")] + [ValidateNotNullOrEmpty()] + [string]$Identifier, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [pscredential]$Credential, + + [Parameter(Mandatory = $false, ParameterSetName = "Shared")] + [switch]$Shared + ) + + # First set a constand path for private CredentialStore mode. + if ($PSCmdlet.ParameterSetName -eq "Private") { + $Path = "{0}\CredentialStore.json" -f $env:APPDATA + } + + # Lets do a quick test on the given CredentialStore. + if (-not(Test-CredentialStore -Path $Path)) { + $MessageParams = @{ + Message = "Could not add anything into the given CredentailStore." + ErrorAction = "Stop" + } + Write-Error @MessageParams + } + + # Read the file content based on the given ParameterSetName + $CSContent = Get-CredentialStore -Path $Path + + $CurrentDate = Get-Date -UFormat "%Y-%m-%d %H:%M:%S" + + if ($Identifier -ne "") { + $CredentialName = $RemoteHost = "{0}/{1}" -f $Identifier, $RemoteHost + } + else { + $CredentialName = $RemoteHost + } + + if (-not($Credential)) { + $Credential = Get-Credential -Message $CredentialName + } + + if ($Credential.UserName) { + if ($CSContent.Type -eq "Shared") { + $Key = Get-ChallengeFile + $encypted = ConvertFrom-SecureString -SecureString $Credential.Password -Key $Key + } + else { + $encypted = ConvertFrom-SecureString -SecureString $Credential.Password + } + if (Get-Member -InputObject $CSContent -Name $CredentialName -Membertype Properties) { + $MessageParams = @{ + Message = "The given host already exists. Nothing to do here." + } + Write-Warning @MessageParams + } + else { + $CredentialHash = [ordered]@{ + User = $Credential.UserName + Password = $encypted + Creation = $CurrentDate + } + Add-Member -InputObject $CSContent -Name $CredentialName -MemberType NoteProperty -Value $CredentialHash + try { + ConvertTo-Json -InputObject $CSContent | Out-File -FilePath $Path + } + catch [System.Exception] { + $MessageParams = @{ + Message = "Couldn't add item into credential store!" + ErrorAction = "Stop" + } + Write-Error @MessageParams + } + } + } + else { + $MessageParams = @{ + Message = "Please Provide at least a valid user!" + ErrorAction = "Stop" + } + Write-Error @MessageParams + } +} diff --git a/src/Item/Remove-CredentialStoreItem.ps1 b/src/Item/Remove-CredentialStoreItem.ps1 new file mode 100644 index 0000000..b82cd27 --- /dev/null +++ b/src/Item/Remove-CredentialStoreItem.ps1 @@ -0,0 +1,94 @@ +function Remove-CredentialStoreItem { + <# + .SYNOPSIS + Remove the given credentials from the credential store. + + .DESCRIPTION + Use this CMDLet to completely remove an credential store item. + + .PARAMETER Path + Define the store in which your given host entry already exists. + + .PARAMETER RemoteHost + Specify the host you for which you would like to change the credentials. + + .PARAMETER Identifier + Defaults to "". Specify a string, which separates two CredentialStoreItems for the + same hostname. + + .PARAMETER Shared + Switch to shared mode with this param. This enforces the command to work with a shared CredentialStore which + can be decrypted across systems. + + .INPUTS + [None] + + .OUTPUTS + [None] + + .EXAMPLE + Remove-CredentialStoreItem -Path "C:\TMP\mystore.json" -RemoteHost "esx01.myside.local" + Remove-CredentialStoreItem -Path "C:\TMP\mystore.json" -RemoteHost "esx01.myside.local" -Identifier svc + + .NOTES + File Name : Remove-CredentialStoreItem.ps1 + Author : Marco Blessing - marco.blessing@googlemail.com + Requires : + + .LINK + https://github.com/OCram85/PSCredentialStore + #> + + [CmdletBinding(DefaultParameterSetName = "Private")] + param( + [Parameter(Mandatory = $false, ParameterSetName = "Shared")] + [string]$Path = "{0}\PSCredentialStore\CredentialStore.json" -f $env:ProgramData, + + [Parameter(Mandatory = $true, ParameterSetName = "Private")] + [Parameter(Mandatory = $true, ParameterSetName = "Shared")] + [string]$RemoteHost, + + [Parameter(Mandatory = $false, ParameterSetName = "Private")] + [Parameter(Mandatory = $false, ParameterSetName = "Shared")] + [string]$Identifier, + + [Parameter(Mandatory = $false, ParameterSetName = "Shared")] + [switch]$Shared + ) + + # First set a constand path for private CredentialStore mode. + if ($PSCmdlet.ParameterSetName -eq "Private") { + $Path = "{0}\CredentialStore.json" -f $env:APPDATA + } + + # Lets do a quick test on the given CredentialStore. + if (-not(Test-CredentialStore -Path $Path)) { + $MessageParams = @{ + Message = "Could not add anything into the given CredentailStore." + ErrorAction = "Stop" + } + Write-Error @MessageParams + } + + # Read the file content based on the given ParameterSetName + $CSContent = Get-CredentialStore -Path $Path + + if ($Identifier -ne "") { + $CredentialName = $RemoteHost = "{0}/{1}" -f $Identifier, $RemoteHost + } + else { + $CredentialName = $RemoteHost + } + + if (Get-Member -InputObject $CSContent -Name $CredentialName -Membertype Properties) { + # We need to use the .NET Method because there is no easier way in PowerShell. + $CSContent.PSObject.Properties.Remove($CredentialName) + ConvertTo-Json -InputObject $CSContent | Out-File -FilePath $Path + } + else { + $MessageParams = @{ + Message = "The given CredentailStoreItem does not exist." + } + Write-Warning @MessageParams + } +} diff --git a/src/Item/Set-CredentialStoreItem.ps1 b/src/Item/Set-CredentialStoreItem.ps1 new file mode 100644 index 0000000..6f35fe8 --- /dev/null +++ b/src/Item/Set-CredentialStoreItem.ps1 @@ -0,0 +1,114 @@ +function Set-CredentialStoreItem { + <# + .SYNOPSIS + Changes the credentials for the given remote host in the store. + + .DESCRIPTION + + .PARAMETER Path + Define the store in which your given host entry already exists. + + .PARAMETER RemoteHost + Specify the host you for which you would like to change the credentials. + + .PARAMETER Identifier + Defaults to "". Specify a string, which separates two CredentialStoreItems for the + same hostname. + + .PARAMETER Shared + Switch to shared mode with this param. This enforces the command to work with a shared CredentialStore which + can be decrypted across systems. + + .INPUTS + [None] + + .OUTPUTS + [None] + + .EXAMPLE + Set-CredentialStoreItem -Path "C:\TMP\mystore.json" -RemoteHost "esx01.myside.local" + Set-CredentialStoreItem -Path "C:\TMP\mystore.json" -RemoteHost "esx01.myside.local" -Identifier svc + + .NOTES + File Name : Set-CredentialStoreItem.ps1 + Author : Marco Blessing - marco.blessing@googlemail.com + Requires : + + .LINK + https://github.com/OCram85/PSCredentialStore + #> + + [CmdletBinding(DefaultParameterSetName = "Private")] + param( + [Parameter(Mandatory = $false, ParameterSetName = "Shared")] + [string]$Path = "{0}\PSCredentialStore\CredentialStore.json" -f $env:ProgramData, + + [Parameter(Mandatory = $true, ParameterSetName = "Private")] + [Parameter(Mandatory = $true, ParameterSetName = "Shared")] + [string]$RemoteHost, + + [Parameter(Mandatory = $false, ParameterSetName = "Private")] + [Parameter(Mandatory = $false, ParameterSetName = "Shared")] + [string]$Identifier, + + [Parameter(Mandatory = $false, ParameterSetName = "Shared")] + [switch]$Shared + ) + + # First set a constant path for private CredentialStore mode. + if ($PSCmdlet.ParameterSetName -eq "Private") { + $Path = "{0}\CredentialStore.json" -f $env:APPDATA + } + + # Lets do a quick test on the given CredentialStore. + if (-not(Test-CredentialStore -Path $Path)) { + $MessageParams = @{ + Message = "Could not add anything into the given CredentailStore." + ErrorAction = "Stop" + } + Write-Error @MessageParams + } + + # Read the file content based on the given ParameterSetName + $CSContent = Get-CredentialStore -Path $Path + + $CurrentDate = Get-Date -UFormat "%Y-%m-%d %H:%M:%S" + + if ($Identifier -ne "") { + $CredentialName = $RemoteHost = "{0}/{1}" -f $Identifier, $RemoteHost + } + else { + $CredentialName = $RemoteHost + } + + $Creds = Get-Credential -Message $CredentialName + + if ($Creds.UserName) { + if ($CSContent.Type -eq "Shared") { + $Key = Get-ChallengeFile + $encypted = ConvertFrom-SecureString -SecureString $Creds.Password -Key $Key + } + else { + $encypted = ConvertFrom-SecureString -SecureString $Creds.Password + } + if (Get-Member -InputObject $CSContent -Name $CredentialName -Membertype Properties) { + $CSContent.$CredentialName.User = $Creds.UserName + $CSContent.$CredentialName.Password = $encypted + $CSContent.$CredentialName.Creation = $CurrentDate + ConvertTo-Json -InputObject $CSContent | Out-File -FilePath $Path + } + else { + $MessageParams = @{ + Message = "The given CredentailStoreItem does not exist." + } + Write-Warning @MessageParams + } + } + Else { + $MessageParams = @{ + Message = "Please Provide at least a valid user!" + ErrorAction = "Stop" + } + Write-Error @MessageParams + } +} diff --git a/src/Item/Test-CredentialStoreItem.ps1 b/src/Item/Test-CredentialStoreItem.ps1 new file mode 100644 index 0000000..56e9c9d --- /dev/null +++ b/src/Item/Test-CredentialStoreItem.ps1 @@ -0,0 +1,93 @@ +function Test-CredentialStoreItem() { + <# + .SYNOPSIS + Checks if the given RemoteHost identifier combination exists in the credential store. + + .DESCRIPTION + Use this cmdlet for basic checks with a single item. Check the item first with this function before + you try to interact with it. + + .PARAMETER Path + Define a custom credential store you try to read from. Without the `-Path` parameter + `Test-CredentialStoreItem` tries to read from the default private store. + + .PARAMETER RemoteHost + Specify the host, for which you would like to change the credentials. + + .PARAMETER Identifier + Adds an optional identifier to the given RemoteHost. Makes it possible to store multiple credentials + for a single host. + + .PARAMETER Shared + Switch to shared mode with this param. This enforces the command to work with a shared CredentialStore which + can be decrypted across systems. + + .INPUTS + [None] + + .OUTPUTS + [None] + + .EXAMPLE + If (Test-CredentialStoreItem -RemoteHost "Default") { + Get-CredentialStoreItem -RemoteHost "Default" + } + Else { + Write-Warning ("The given Remote Host {0} does not exist in the credential Store!" -f $RemoteHost) + } + + .NOTES + File Name : Test-CredentialStoreItem.ps1 + Author : Marco Blessing - marco.blessing@googlemail.com + Requires : + + .LINK + https://github.com/OCram85/PSCredentialStore + #> + [CmdletBinding(DefaultParameterSetName = "Private")] + [OutputType([Boolean])] + param( + [Parameter(Mandatory = $false, ParameterSetName = "Shared")] + [string]$Path = "{0}\PSCredentialStore\CredentialStore.json" -f $env:ProgramData, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$RemoteHost, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string]$Identifier, + + [Parameter(Mandatory = $false, ParameterSetName = "Shared")] + [switch]$Shared + ) + + if ($PSCmdlet.ParameterSetName -eq "Private") { + $Path = "{0}\CredentialStore.json" -f $env:APPDATA + } + + if ($Identifier -ne "") { + $CredentialName = $RemoteHost = "{0}/{1}" -f $Identifier, $RemoteHost + } + else { + $CredentialName = $RemoteHost + } + + if (Test-CredentialStore -Path $Path) { + $CS = Get-CredentialStore -Path $Path + $CSMembers = Get-Member -InputObject $CS + if (($CSMembers.MemberType -eq "NoteProperty") -and ($CSMembers.Name -eq $CredentialName)) { + return $true + } + else { + return $false + } + } + else { + $MsgParams = @{ + ErrorAction = "Stop" + Message = "The given credential store ({0}) does not exist!" -f $Path + } + Write-Error @MsgParams + } +} diff --git a/src/PSCredentialStore.psd1 b/src/PSCredentialStore.psd1 new file mode 100644 index 0000000..54b8c64 --- /dev/null +++ b/src/PSCredentialStore.psd1 @@ -0,0 +1,140 @@ +# +# Module manifest for module 'PSCredentialStore' +# +# Generated by: OCram85 +# +# Generated on: 27.07.2017 +# + +@{ + + # Script module or binary module file associated with this manifest. + RootModule = 'PSCredentialStore' + + # Version number of this module. + # Do not touch the version number. It gets replaced in the build process. + ModuleVersion = '0.0.0.9999' + + # Supported PSEditions + # CompatiblePSEditions = @() + + # ID used to uniquely identify this module + GUID = '6800e192-9df8-4e30-b253-eb2c799bbe84' + + # Author of this module + Author = 'OCram85' + + # Company or vendor of this module + CompanyName = '' + + # Copyright statement for this module + Copyright = '(c) 2017 OCram85. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'A simple credential manager to store and reuse multiple credential objecs' + + # Minimum version of the Windows PowerShell engine required by this module + PowerShellVersion = '4.0' + + # Name of the Windows PowerShell host required by this module + # PowerShellHostName = '' + + # Minimum version of the Windows PowerShell host required by this module + # PowerShellHostVersion = '' + + # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # DotNetFrameworkVersion = '' + + # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # CLRVersion = '' + + # Processor architecture (None, X86, Amd64) required by this module + # ProcessorArchitecture = '' + + # Modules that must be imported into the global environment prior to importing this module + # RequiredModules = @() + + # Assemblies that must be loaded prior to importing this module + # RequiredAssemblies = @() + + # Script files (.ps1) that are run in the caller's environment prior to importing this module. + # ScriptsToProcess = @() + + # Type files (.ps1xml) to be loaded when importing this module + # TypesToProcess = @() + + # Format files (.ps1xml) to be loaded when importing this module + # FormatsToProcess = @() + + # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess + # NestedModules = @() + + # 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. + FunctionsToExport = @( + # Connection Group + 'Connect-To', + 'Disconnect-From', + # Item Group + 'Get-CredentialStoreItem', + 'Set-CredentialStoreItem', + 'New-CredentialStoreItem', + 'Remove-CredentialStoreItem', + 'Test-CredentialStoreItem', + # Store Group + 'Get-CredentialStore', + 'New-CredentialStore', + 'Test-CredentialStore' + + ) + + # 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. + CmdletsToExport = @() + + # Variables to export from this module + VariablesToExport = '*' + + # 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. + AliasesToExport = @() + + # DSC resources to export from this module + # DscResourcesToExport = @() + + # List of all modules packaged with this module + # ModuleList = @() + + # List of all files packaged with this module + # FileList = @() + + # 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. + PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('Credential Store', + 'Credential Manager' + ) + + # A URL to the license for this module. + LicenseUri = 'https://github.com/OCram85/PSCredentialStore/blob/master/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/OCram85/PSCredentialStore' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + ReleaseNotes = 'This is a draft version / pre-release. Do not use in production!' + + } # End of PSData hashtable + + } # End of PrivateData hashtable + + # HelpInfo URI of this module + HelpInfoURI = 'https://github.com/OCram85/PSCredentialStore' + + # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. + # DefaultCommandPrefix = '' + +} diff --git a/src/PSCredentialStore.psm1 b/src/PSCredentialStore.psm1 new file mode 100644 index 0000000..61866b5 --- /dev/null +++ b/src/PSCredentialStore.psm1 @@ -0,0 +1,10 @@ +$Items = (Get-ChildItem -Path ("{0}\*.ps1" -f $PSScriptRoot ) -Recurse ).FullName | Where-Object { + $_ -notmatch "(Classes|Init)" +} +ForEach ($Item in $Items) { + # Write-Verbose ("dot sourcing file {0}" -f $Item) + . $Item +} + +# Exports are now controlled by module manifest +# Export-ModuleMember -Function * diff --git a/src/Store/Get-CredentialStore.ps1 b/src/Store/Get-CredentialStore.ps1 new file mode 100644 index 0000000..8828d92 --- /dev/null +++ b/src/Store/Get-CredentialStore.ps1 @@ -0,0 +1,69 @@ +function Get-CredentialStore { + <# + .SYNOPSIS + Reads the complete content of the credential store and returns it as a new object. + + .DESCRIPTION + The content is in a raw format. It means there is no transformation to the different credential types. + You can not use the object properties to connect with remote host. Therefore please use + Get-CredentialStoreItem. + + .PARAMETER Path + Define a custom path to a shared CredentialStore. + + .PARAMETER Shared + Switch to shared mode with this param. This enforces the command to work with a shared CredentialStore which + can be decrypted across systems. + + .INPUTS + [None] + + .OUTPUTS + [PSObject] Returns the credential store content as PSObject. + + .EXAMPLE + $CSContent = Get-CredentialStore -Path "C:\TMP\mystore.json" + + .NOTES + File Name : Get-CredentialStore.ps1 + Author : Marco Blessing - marco.blessing@googlemail.com + Requires : + + .LINK + https://github.com/OCram85/PSCredentialStore + #> + + [CmdletBinding(DefaultParameterSetName = "Private")] + param( + [Parameter(Mandatory = $false, ParameterSetName = "Shared")] + [string]$Path = "{0}\PSCredentialStore\CredentialStore.json" -f $env:ProgramData, + + [Parameter(Mandatory = $false, ParameterSetName = "Shared")] + [switch]$Shared + ) + + if ($PSCmdlet.ParameterSetName -eq 'Private') { + $Path = "{0}\CredentialStore.json" -f $env:APPDATA + } + + if (Test-CredentialStore -Path $Path) { + try { + $FileContent = Get-Content -Path $Path -Raw + ConvertFrom-Json $FileContent + } + catch [System.Exception] { + $MessageParams = @{ + Message = "Unknown CredentialStore format. Invalid JSON file." + ErrorAction = "Stop" + } + Write-Error @MessageParams + } + } + else { + $MessageParams = @{ + Message = "Could not find the CredentialStore." + ErrorAction = "Stop" + } + Write-Error @MessageParams + } +} diff --git a/src/Store/New-CredentialStore.ps1 b/src/Store/New-CredentialStore.ps1 new file mode 100644 index 0000000..6bbe19f --- /dev/null +++ b/src/Store/New-CredentialStore.ps1 @@ -0,0 +1,111 @@ +function New-CredentialStore { + <# + .SYNOPSIS + Creates a new credential store File + + .DESCRIPTION + You need to run this script first to create a new credential store before you try to + save new credentials with New-CredentialStoreItem. + + .PARAMETER Path + Define a location for the new shared CredentialStore. The default store will be created in + $Env:ProgramData\PSCredentialStore dir. + + .PARAMETER Shared + Creates a CredentialStore in the Shared mode. This enables you to read the CredentialStore Items on + different systems or profiles. In addition you can optionally provide a custom path wit the -Path parameter. + + .PARAMETER Force + Use this switch to reset an existing store. The complete content will be wiped. + + .INPUTS + [None] + + .OUTPUTS + [None] + + .EXAMPLE + New-CredentialStore + # Creates a new private CredentialStore + + .EXAMPLE + New-CredentialStore -Force + # Resets an existing private CredentialStore + + .EXAMPLE + New-CredentialStore -Shared + # Creates a new shared CredentialStore + + .EXAMPLE + New-CredentialStore -Shared -Path "C:\TMP\CredentialStore.json" + # Creates a new shared CredentialStore in the given location. + + .NOTES + File Name : New-CredentialStore.ps1 + Author : Marco Blessing - marco.blessing@googlemail.com + Requires : + + .LINK + https://github.com/OCram85/PSCredentialStore + #> + + [CmdletBinding(DefaultParameterSetName = "Private")] + param( + [Parameter(Mandatory = $false, ParameterSetName = "Shared")] + [switch]$Shared, + + [Parameter(Mandatory = $false, ParameterSetName = "Shared")] + [ValidateNotNullOrEmpty()] + [string]$Path = "{0}\PSCredentialStore\CredentialStore.json" -f $env:ProgramData, + + [Parameter(Mandatory = $false, ParameterSetName = "Private")] + [Parameter(Mandatory = $false, ParameterSetName = "Shared")] + [switch]$Force + ) + + # Lets get the current Date in a human readable format. + $CurrentDate = Get-Date -UFormat "%Y-%m-%d %H:%M:%S" + + # Set latest Credential Store version + Set-Variable -Name "CSVersion" -Value "1.2.0" -Option Constant + + # Set the CredentialStore path for private mode. + Write-Debug ("ParameterSetName: {0}" -f $PSCmdlet.ParameterSetName) + if ($PSCmdlet.ParameterSetName -eq "Private") { + $Path = "{0}\CredentialStore.json" -f $Env:APPDATA + } + + # Test if in the given store already a CredentialStore exists. + Write-Verbose "Test if there is already a credential store." + if ((Test-CredentialStore -Path $Path) -and ($Force -ne $true)) { + $MessageParam = @{ + Message = "The given file already exists. Use the 'Force' switch to override the existing store." + ErrorAction = "Stop" + } + Write-Error @MessageParam + } + # We need to use the IDictionary to keep the property sorting in the object. + $ObjProperties = [ordered]@{ + Version = $CSVersion + Creation = $CurrentDate + } + if ($PSCmdlet.ParameterSetName -eq "Shared") { + $ObjProperties.Type = "Shared" + # Check if a ChallengeFile already exists. We don't want to overide it. + # Otherwise previous created CredentialStores couln't be decrypted anymore. + if (-not (Test-ChallengeFile)) { + Set-ChallengeFile + } + } + else { + $ObjProperties.Type = "Private" + } + # Create a new object for easy conversion into a json file + $CredentialStoreObj = New-Object -TypeName psobject -Property $ObjProperties + try { + ConvertTo-Json -InputObject $CredentialStoreObj | Out-File -FilePath $Path + } + catch [System.Exception] { + $_.Exception | Format-List -Force | Out-String | Write-Error -ErrorAction Stop + } +} diff --git a/src/Store/Test-CredentialStore.ps1 b/src/Store/Test-CredentialStore.ps1 new file mode 100644 index 0000000..cf72109 --- /dev/null +++ b/src/Store/Test-CredentialStore.ps1 @@ -0,0 +1,61 @@ +function Test-CredentialStore { + <# + .SYNOPSIS + Returns the credential store state. + + .DESCRIPTION + Use this script to test your credential store. For now it only checks if + the file exists. + + .PARAMETER Path + Define a custom path to a shared CredentialStore. + + .PARAMETER Shared + Switch to shared mode with this param. This enforces the command to work with a shared CredentialStore which + can be decrypted across systems. + + .NOTES + File Name : Test-CredentialStore.ps1 + Author : Marco Blessing - marco.blessing@googlemail.com + Requires : + + .LINK + https://github.com/OCram85/PSCredentialStore + #> + [CmdletBinding(DefaultParameterSetName = "Private")] + param( + [Parameter(Mandatory = $false, ParameterSetName = "Shared")] + [string]$Path = "{0}\PSCredentialStore\CredentialStore.json" -f $env:ProgramData, + + [Parameter(Mandatory = $false, ParameterSetName = "Shared")] + [switch]$Shared + ) + + + if ($PSCmdlet.ParameterSetName -eq "Private") { + $Path = "{0}\CredentialStore.json" -f $Env:APPDATA + } + + # Set latest Credential Store version + Set-Variable -Name "CSVersion" -Value "1.2.0" -Option Constant + + if (Test-Path $Path) { + Write-Verbose "CredentialStore in given path found." + + # try tor read the store. Removed the Get-CredentialStore function to avoid recursive calls. + try { + $FileContent = Get-Content -Path $Path -Raw + $CSContent = ConvertFrom-Json $FileContent + } + catch { + Write-Warning "Could not read or convert the given CredentialStore." + Return $False + } + Return $True + + } + Else { + Write-Verbose "The given CredentialStore does not exist!" + Return $False + } +} diff --git a/tests/.gitkeep b/tests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/Item/01_New-CredentialStoreItem.Tests.ps1 b/tests/Item/01_New-CredentialStoreItem.Tests.ps1 new file mode 100644 index 0000000..9624c2b --- /dev/null +++ b/tests/Item/01_New-CredentialStoreItem.Tests.ps1 @@ -0,0 +1,65 @@ +#region HEADER +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +# $RepoRoot = (Get-Item -Path $here).Parent.Parent.FullName +$RepoRoot = (Get-GitDirectory).replace('\.git', '') +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.' +$sut = $sut -replace "\d{2}`_", '' +$suthome = (Get-ChildItem -Path $RepoRoot -Exclude ".\tests\" -Filter $sut -Recurse).FullName +# Skip try loading the source file if it doesn't exists. +If ($suthome.Length -gt 0) { + . $suthome +} +Else { + Write-Warning ("Could not find source file {0}" -f $sut) +} + +# load additional functions defined in the repository. Replace the expression . +. (Get-ChildItem -Path $RepoRoot -Filter "Test-CredentialStore.ps1" -Recurse).FullName +. (Get-ChildItem -Path $RepoRoot -Filter "New-CredentialStore.ps1" -Recurse).FullName +. (Get-ChildItem -Path $RepoRoot -Filter "Get-CredentialStore.ps1" -Recurse).FullName +. (Get-ChildItem -Path $RepoRoot -Filter "Get-CredentialStoreItem.ps1" -Recurse).FullName +. (Get-ChildItem -Path $RepoRoot -Filter "Test-ChallengeFile.ps1" -Recurse).FullName +. (Get-ChildItem -Path $RepoRoot -Filter "Get-ChallengeFile.ps1" -Recurse).FullName +. (Get-ChildItem -Path $RepoRoot -Filter "Set-ChallengeFile.ps1" -Recurse).FullName +. (Get-ChildItem -Path $RepoRoot -Filter "Get-RandomKey.ps1" -Recurse).FullName + +#endregion HEADER + +Describe "New-CredentialStoreItem" { + Context "Private Credential Store tests" { + It "Test1: Add entry to existing private store." { + If (-not (Test-CredentialStore)) { + New-CredentialStore + } + [String]$tmp = (65..90) + (97..122) | Get-Random -Count 5 | % {[char]$_} + $tmp = $tmp.Replace(' ', '') + $tmpUser = "MyUser" + $tmpPwd = "fooobarysdfsfs" | ConvertTo-SecureString -AsPlainText -Force + $creds = New-Object -TypeName PsCredential -ArgumentList $tmpUser, $tmpPwd + New-CredentialStoreItem -RemoteHost $tmp -Credential $creds + # Had to remove the `{ } | Shoud Not Throw` because the return would be empty. + $content = Get-CredentialStoreItem -RemoteHost $tmp + $content.UserName | Should Be "MyUser" + #Cleanup Temp entry + $CS = Get-CredentialStore + $CS.PSObject.Properties.Remove($tmp) + ConvertTo-Json -InputObject $CS | Out-File -FilePath ("{0}\CredentialStore.json" -f $env:AppData) + } + } + Context "Test with new shared Credential Store" { + It "Test2: Create new RemoteHost entry" { + # prepare test environment + $tmpCS = 'C:\CredentialStore.json' + New-CredentialStore -Shared -Path $tmpCS + + $UserName = "myuser" + $Password = ConvertTo-SecureString -String "mypasswd" -AsPlainText -Force + $mycreds = New-Object -TypeName PSCredential -ArgumentList $UserName, $Password + $RemoteHost = "foobar" + { New-CredentialStoreItem -Path $tmpCS -RemoteHost $RemoteHost -Credential $mycreds -Shared } | Should Not Throw + $tmpCS = Get-Content -Path $tmpCS -Raw | ConvertFrom-Json + $res = Get-Member -InputObject $tmpCS -Name $RemoteHost -Membertype Properties + $res.Name | Should Be $RemoteHost + } + } +} diff --git a/tests/Store/00_Get-CredentialStore.Tests.ps1 b/tests/Store/00_Get-CredentialStore.Tests.ps1 new file mode 100644 index 0000000..5fbe500 --- /dev/null +++ b/tests/Store/00_Get-CredentialStore.Tests.ps1 @@ -0,0 +1,37 @@ +#region HEADER +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +# $RepoRoot = (Get-Item -Path $here).Parent.Parent.FullName +$RepoRoot = (Get-GitDirectory).replace('\.git', '') +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.' +$sut = $sut -replace "\d{2}`_", '' +$suthome = (Get-ChildItem -Path $RepoRoot -Exclude ".\tests\" -Filter $sut -Recurse).FullName +# Skip try loading the source file if it doesn't exists. +If ($suthome.Length -gt 0) { + . $suthome +} +Else { + Write-Warning ("Could not find source file {0}" -f $sut) +} + +# load additional functions defined in the repository. Replace the expression . +. (Get-ChildItem -Path $RepoRoot -Filter "Test-CredentialStore.ps1" -Recurse).FullName + +#endregion HEADER + +Describe "Get-CredentialStore" { + Context "Basic logic tests" { + $TestCredentialStore = Resolve-Path -Path ("{0}\resources\cs\CredentialStore.json" -f $RepoRoot) + It "Test1: Read CS without params" { + If (Test-Path -Path ("{0}\CredentialStore.json" -f $env:APPDATA)) { + {Get-CredentialStore} | Should Not Throw + } + Else { + Write-Warning "Default private Credential Store not found. Skipping..." + } + } + It "Test2: Read Credential Store with testing data" { + + {Get-CredentialStore -Path $TestCredentialStore} | Should Not Throw + } + } +} diff --git a/tests/Store/00_New-CredentialStore.Tests.ps1 b/tests/Store/00_New-CredentialStore.Tests.ps1 new file mode 100644 index 0000000..e633374 --- /dev/null +++ b/tests/Store/00_New-CredentialStore.Tests.ps1 @@ -0,0 +1,114 @@ +#region HEADER +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +# $RepoRoot = (Get-Item -Path $here).Parent.Parent.FullName +$RepoRoot = (Get-GitDirectory).replace('\.git', '') +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.' +$sut = $sut -replace "\d{2}`_", '' +$suthome = (Get-ChildItem -Path $RepoRoot -Exclude ".\tests\" -Filter $sut -Recurse).FullName +# Skip try loading the source file if it doesn't exists. +If ($suthome.Length -gt 0) { + . $suthome +} +Else { + Write-Warning ("Could not find source file {0}" -f $sut) +} + +# load additional functions defined in the repository. Replace the expression . +. (Get-ChildItem -Path $RepoRoot -Filter "Test-CredentialStore.ps1" -Recurse).FullName +. (Get-ChildItem -Path $RepoRoot -Filter "Test-ChallengeFile.ps1" -Recurse).FullName +. (Get-ChildItem -Path $RepoRoot -Filter "Set-ChallengeFile.ps1" -Recurse).FullName +. (Get-ChildItem -Path $RepoRoot -Filter "Get-RandomKey.ps1" -Recurse).FullName + +#endregion HEADER + + +# Backup existing credential stores +$VerbosePreference = "Continue" +Write-Verbose "Backup private Credential Store..." +$CSPath = ("{0}\CredentialStore.json" -f $env:APPDATA) +$BackupFile = "{0}.back" -f $CSPath +If (Test-Path -Path $CSPath) { + Move-Item -Path $CSPath -Destination $BackupFile +} +Write-Verbose "Backup shared CredentialStore..." +$CSShared = ("{0}\PSCredentialStore\CredentialStore.json" -f $env:ProgramData) +$BackupSharedFile = "{0}.back" -f $CSShared +If (Test-Path -Path $CSShared) { + Move-Item -Path $CSShared -Destination $BackupSharedFile +} +Write-Verbose "Remove old CredentialStore in Temp dir" +$CSTemp = "{0}\CredentialStore.json" -f $Env:TEMP +If (Test-Path -Path $CSTemp) { + Remove-Item -Path $CSTemp +} +$VerbosePreference = "SilentlyContinue" + +Describe "New-CredentialStore" { + Context "Private CS tests" { + $pCS = Join-Path -Path $env:APPDATA -ChildPath "CredentialStore.json" + It "Test1: Create new private CredentialStore" { + New-CredentialStore + $result = Test-Path -Path $pCS + $CS = Get-Content -Path $pCS -Raw | ConvertFrom-Json -ErrorAction SilentlyContinue + ($result -eq $True) -and ($CS.Type -eq "Private") | Should Be $True + } + It "Test2: Try to override private Store" { + {New-CredentialStore} | Should Throw + } + It "Test3: Reset existing Credential Store" { + $now = Get-Date + $CS = Get-Content -Path $pCS -Raw | ConvertFrom-Json + $CSCreation = [DateTime]$CS.Creation + New-CredentialStore -Force + $now -gt $csCreation | Should Be $True + } + } + Context "Shared CS tests" { + $pCS = Join-Path -Path $env:ProgramData -ChildPath "PSCredentialStore\CredentialStore.json" + It "Test1: Create a new Shared Credential Store" { + New-CredentialStore -Shared + Test-Path -Path ("{0}\PSCredentialStore\CredentialStore.json" -f $env:ProgramData) | Should Be $True + } + It "Test2: Try to override existing shared CS" { + {New-CredentialStore -Shared} | Should Throw + } + It "Test3: Reset shared CredentialStore" { + $now = Get-Date + $CS = Get-Content -Path $pCS -Raw | ConvertFrom-Json + $CSCreation = [DateTime]$CS.Creation + New-CredentialStore -Force -Shared + $now -gt $csCreation | Should Be $True + } + } + Context "Custom Shared CS tests" { + $pCS = Join-Path -Path $env:TEMP -ChildPath "CredentialStore.json" + It "Test1: Create new custom shared" { + {New-CredentialStore -Path $pCS -Shared} | Should Not Throw + } + It "Test2: Try to override exiting one" { + {New-CredentialStore -Path $pCS -Shared} | Should Throw + } + It "Test3: Reset existing custom CredentialStore" { + {New-CredentialStore -Path $pCS -Shared -Force} | Should Not Throw + } + } +} + +# Cleanup test stores and restore existing ones. +$VerbosePreference = "Continue" +Write-Verbose "Restoring private CredentialStore" +If (Test-Path -Path $BackupFile) { + If (Test-Path -Path $CSPath) { + Remove-Item -Path $CSPath + Move-Item -Path $BackupFile -Destination $CSPath + } +} + +Write-Verbose "Restoring shared CredentialStore" +If (Test-Path -Path $BackupSharedFile) { + If (Test-Path -Path $CSShared) { + Remove-Item -Path $CSShared + Move-Item -Path $BackupSharedFile -Destination $CSShared + } +} +$VerbosePreference = "SilentlyContinue" diff --git a/tests/Store/00_Test-CredentialStore.Tests.ps1 b/tests/Store/00_Test-CredentialStore.Tests.ps1 new file mode 100644 index 0000000..693184f --- /dev/null +++ b/tests/Store/00_Test-CredentialStore.Tests.ps1 @@ -0,0 +1,40 @@ +#region HEADER +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +# $RepoRoot = (Get-Item -Path $here).Parent.Parent.FullName +$RepoRoot = (Get-GitDirectory).replace('\.git', '') +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.' +$sut = $sut -replace "\d{2}`_", '' +$suthome = (Get-ChildItem -Path $RepoRoot -Exclude ".\tests\" -Filter $sut -Recurse).FullName +# Skip try loading the source file if it doesn't exists. +If ($suthome.Length -gt 0) { + . $suthome +} +Else { + Write-Warning ("Could not find source file {0}" -f $sut) +} + +# load additional functions defined in the repository. Replace the expression . +# . (Get-ChildItem -Path $RepoRoot -Filter ".ps1" -Recurse).FullName + +#endregion HEADER + +Describe "Test-CredentialStore" { + Context "Basic logic tests" { + $TestCredentialStore = Resolve-Path -Path ("{0}\resources\cs\CredentialStore.json" -f $RepoRoot) + It "Test1: Should Not Throw" { + { Test-CredentialStore -Path $TestCredentialStore } | Should Not Throw + } + It "Test2: Read valid CredentialStore" { + $res = Test-CredentialStore -Path $TestCredentialStore + $res | Should Be $True + } + It "Test3: Read a broken CredentialStore" { + $BrokenCS = Resolve-Path -Path ("{0}\resources\cs\Broken_CS.json" -f $RepoRoot) + $oWarningPreference = $WarningPreference + $WarningPreference = 'SilentlyContinue' + $res = Test-CredentialStore -Path $BrokenCS + $res | Should Be $False + $WarningPreference = $oWarningPreference + } + } +} diff --git a/tools/AppVeyor.psm1 b/tools/AppVeyor.psm1 new file mode 100644 index 0000000..58f916d --- /dev/null +++ b/tools/AppVeyor.psm1 @@ -0,0 +1,178 @@ +<# + Define the callsign of you PowerShell Module. + Callsign is used to identity: + - Module Manifest file name + - Artifact File + - Git repository name + - Module name +#> +$CALLSIGN = 'PSCredentialStore' +Write-Host ("Callsign is: {0}" -f $CALLSIGN) -ForegroundColor Yellow + +Function Invoke-AppVeyorBumpVersion() { + [CmdletBinding()] + Param() + + Write-Host "Listing Env Vars for debugging:" -ForegroundColor Yellow + # Filter Results to prevent exposing secure vars. + Get-ChildItem -Path "Env:*" | Where-Object { $_.name -notmatch "(NuGetToken|CoverallsToken)"} | Sort-Object -Property Name | Format-Table + + Try { + $ModManifest = Get-Content -Path (".\src\{0}.psd1" -f $CALLSIGN) + $BumpedManifest = $ModManifest -replace '0.0.0.9999', $Env:APPVEYOR_BUILD_VERSION + Remove-Item -Path (".\src\{0}.psd1" -f $CALLSIGN) + Out-File -FilePath (".\src\{0}.psd1" -f $CALLSIGN) -InputObject $BumpedManifest -NoClobber -Encoding utf8 -Force + } + Catch { + $MsgParams = @{ + Message = 'Could not bump current version into module manifest.' + Category = 'Error' + Details = $_.Exception.Message + } + Add-AppveyorMessage @MsgParams + Throw $MsgParams.Message + } +} + +Function Invoke-AppVeyorBuild() { + [CmdletBinding()] + Param() + $MsgParams = @{ + Message = 'Creating build artifacts' + Category = 'Information' + Details = 'Extracting source files and compressing them into zip file.' + } + Add-AppveyorMessage @MsgParams + $CompParams = @{ + Path = "{0}\src\*" -f $env:APPVEYOR_BUILD_FOLDER + DestinationPath = "{0}\bin\{1}.zip" -f $env:APPVEYOR_BUILD_FOLDER, $CALLSIGN + Update = $True + Verbose = $True + } + Compress-Archive @CompParams + $MsgParams = @{ + Message = 'Pushing artifacts' + Category = 'Information' + Details = 'Pushing artifacts to AppVeyor store.' + } + Add-AppveyorMessage @MsgParams + Push-AppveyorArtifact (".\bin\{0}.zip" -f $CALLSIGN) +} + +Function Invoke-AppVeyorTests() { + [CmdletBinding()] + Param() + + $MsgParams = @{ + Message = 'Starting Pester tests' + Category = 'Information' + Details = 'Now running all test found in .\tests\ dir.' + } + Add-AppveyorMessage @MsgParams + $testresults = Invoke-Pester -Path ".\tests\*" -ExcludeTag 'Disabled' -PassThru + ForEach ($Item in $testresults.TestResult) { + Switch ($Item.Result) { + "Passed" { + $TestParams = @{ + Name = "{0}: {1}" -f $Item.Context, $Item.Name + Framework = "NUnit" + Filename = $Item.Describe + Outcome = "Passed" + Duration = $Item.Time.Milliseconds + } + Add-AppveyorTest @TestParams + } + "Failed" { + $TestParams = @{ + Name = "{0}: {1}" -f $Item.Context, $Item.Name + Framework = "NUnit" + Filename = $Item.Describe + Outcome = "Failed" + Duration = $Item.Time.Milliseconds + ErrorMessage = $Item.FailureMessage + ErrorStackTrace = $Item.StackTrace + } + Add-AppveyorTest @TestParams + } + Default { + $TestParams = @{ + Name = "{0}: {1}" -f $Item.Context, $Item.Name + Framework = "NUnit" + Filename = $Item.Describe + Outcome = "None" + Duration = $Item.Time.Milliseconds + ErrorMessage = $Item.FailureMessage + ErrorStackTrace = $Item.StackTrace + } + Add-AppveyorTest @TestParams + } + } + } + If ($testresults.FailedCount -gt 0) { + $MsgParams = @{ + Message = 'Pester Tests failed.' + Category = 'Error' + Details = "$($testresults.FailedCount) tests failed." + } + Add-AppveyorMessage @MsgParams + Throw $MsgParams.Message + } + +} + +Function Invoke-CoverageReport() { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $False)] + [ValidateNotNullOrEmpty()] + [String]$RepoToken = $Env:CoverallsToken + ) + + Import-Module ('.\src\{0}.psm1' -f $CALLSIGN) -Verbose -Force + $FileMap = New-PesterFileMap -SourceRoot '.\src' -PesterRoot '.\tests' + $CoverageReport = New-CoverageReport -PesterFileMap $FileMap -RepoToken $RepoToken + Write-Host "CoverageReport JSON:" -ForegroundColor Yellow + $CoverageReport | Out-String | Write-Host + Publish-CoverageReport -CoverageReport $CoverageReport +} + +Function Invoke-AppVeyorPSGallery() { + [CmdletBinding()] + Param() + Expand-Archive -Path (".\bin\{0}.zip" -f $CALLSIGN) -DestinationPath ("C:\Users\appveyor\Documents\WindowsPowerShell\Modules\{0}\" -f $CALLSIGN) -Verbose + Import-Module -Name $CALLSIGN -Verbose -Force + Write-Host "Available Package Provider:" -ForegroundColor Yellow + Get-PackageProvider -ListAvailable + Write-Host "Available Package Sources:" -ForegroundColor Yellow + Get-PackageSource + Try { + Write-Host "Try to get NuGet Provider:" -ForegroundColor Yellow + Get-PackageProvider -Name NuGet -ErrorAction Stop + } + Catch { + Write-Host "Installing NuGet..." -ForegroundColor Yellow + Install-PackageProvider -Name NuGet -MinimumVersion '2.8.5.201' -Force -Verbose + Import-PackageProvider NuGet -MinimumVersion '2.8.5.201' -Force + } + Try { + If ($env:APPVEYOR_REPO_BRANCH -eq 'master') { + Write-Host "try to publish module" -ForegroundColor Yellow + Write-Host ("Callsign is: {0}" -f $CALLSIGN) -ForegroundColor Yellow + Publish-Module -Name $CALLSIGN -NuGetApiKey $env:NuGetToken -Verbose -Force + } + Else { + Write-Host "Skip publishing to PS Gallery because we are on $($env:APPVEYOR_REPO_BRANCH) branch." -ForegroundColor Yellow + # had to remove the publish-Module statement because it would publish although the -WhatIf is given. + # Publish-Module -Name $CALLSIGN -NuGetApiKey $env:NuGetToken -Verbose -WhatIf + } + } + Catch { + $MsgParams = @{ + Message = 'Could not deploy module to PSGallery.' + Category = 'Error' + Details = $_.Exception.Message + } + Add-AppveyorMessage @MsgParams + Throw $MsgParams.Message + } +}