In an earlier post, I introduced you to Pester, a unit testing framework for PowerShell. The idea is to create unit tests and/or integration tests for your code and use it to validate that changes being introduced (either to your code or the upstream target) is still valid for the use case and project.
Before I go too deep down that rabbit hole, however, I thought I’d share how I’m generating private test credentials for automated local testing. This was something I needed to address because I wanted to automate the initiation of the tests themselves while keeping a secure set of encrypted credentials that are only valid for the workstation I’m using. This really boils down to a few steps:
- Creating a PSCredential object with the username and password of an ad-hoc lab account.
- Exporting the PSCredential object into an XML file to make the credentials locally persistent.
I tinkered around with keeping the PSCredential in memory and addressing it with a Global scope, but that seemed overkill for what I’m trying to do. This is fairly simple. I also don’t want anyone else using my secure string and thus avoided using Key/SecureKey. I’m almost positive that security professionals are growing grey hairs from my shenanigans, but I’m not overly concerned about credentials to access my test framework. 🙂
Creating a PSCredential
If you’re just looking for a one-liner, the following code will take a username and password, store them into a PSCredential object, and then export the results into an XML file. In the example below, the username is admin and the password is password.
New-Object System.Management.Automation.PSCredential("admin", (ConvertTo-SecureString -AsPlainText -Force "password")) | Export-CliXml 'C:\TestVars\test-cred.xml'
Here’s what the resulting XML file looks like:
<Objs Version="220.127.116.11" xmlns="http://schemas.microsoft.com/powershell/2004/04">
Using the PSCredential
Use an Import-Clixml cmdlet to pull in the PSCredential information into a $cred variable with automated testing scripts:
$cred = Import-Clixml -Path '.\TestVars\test-cred.xml'
This allows you to run the Invoke-Pester function using a service account that has no authority anywhere in the test environment while also specifying a unique test account located in the XML file. It also runs in an automated fashion – there’s no goofy plain text passwords anywhere, nor a need for a human to enter credentials.
If someone else tries to run the Import-Clixml cmdlet on the file, they’ll see this error:
CryptographicException: Key not valid for use in specified state.
Is it hack proof? No. But certainly seemed cleaner than saving password information in plain text or trying to tie a disparate testing environment directly into my workstation. I’m all ears for better methods, though!
Securing Credentials with AppVeyor
The down side to this method is that it doesn’t work with public repositories. No one wants their password – encrypted or otherwise – available on the web. So with AppVeyor, which is used for continuous integration for the Rubrik PowerShell Module, the idea is to get the credentials using environmental variables. These are available by going to Project > Name of the Project > Settings > Environment > Add variable. An example from the Rubrik PowerShell Module is shown below:
The test cluster IP, test username, and test password are stored in environment variables. When AppVeyor spins up a Windows test server, it sets the variables with the values specified. You can pull out environmental variables in windows by using the prefix $env. The full variable looks like this:
For example, the computer’s NetBIOS name is $env:COMPUTERNAME and the Rubrik Cluster IP is $env:RUBRIKCLUSTER. When Pester runs in the AppVeyor environment, it pulls the necessary testing information from environmental variables instead of the XML file. But hey – how does the test code know where it’s being run?
Determining the Testing Environment
By placing some very simple logic in the testing file, it’s possible to determine the environment. There’s actually a bajillion ways to find out where the code is being run, but the goal was to focus on being efficient while also pulling information. Two birds, one stone.
Here’s a sample snipet of the logic that retrieves the test cluster IP.
if ((Test-Path -Path '.\TestVars\test-ip.txt') -eq $true)
[string]$cluster = Get-Content -Path '.\TestVars\test-ip.txt'
[string]$cluster = $env:RUBRIKCLUSTER
If both tests fail, the unit testing also fails, so I avoided wrapping this with any further error handling. The resulting error is fairly obvious. You’ll see something like this:
[-] Error occurred in test script 'C:\Dropbox\Code\Rubrik\Tests\Connect-Rubrik.Tests.ps1' 101ms
ParameterBindingValidationException: Cannot bind argument to parameter 'String' because it is null
at <ScriptBlock>, C:\Dropbox\Code\Rubrik\Tests\Connect-Rubrik.Tests.ps1: line 21
at <ScriptBlock>, C:\Program Files\WindowsPowerShell\Modules\Pester\3.4.0\Pester.psm1: line 279
at Invoke-Pester, C:\Program Files\WindowsPowerShell\Modules\Pester\3.4.0\Pester.psm1: line 292
at <ScriptBlock>, <No file>: line 1
This is a long way of saying that the variable was null. If desired, test the environmental variable and
throw a descriptive error.