Check your Power Platform solutions for secrets with Pester and PowerShell
/Lately I’ve been doing some work on deployments to the Power Platform. I’ve chosen to use Azure DevOps pipelines as the deployment solution. The reason behind this I leave out of the scope of this blogpost.
When using Azure DevOps you can use GitHub Advanced Security for Azure DevOps but this is quite costly and might be to much for your applications. In our case we just wanted to have a simple scanning for secrets when exporting solution from the DEV environment and deploying them. Just to make sure a developer didn’t accidently use an url with a token in it or have some hard coded username and password in the solution.
To achieve this I used the PowerShell PSSecretScanner module by Björn Sundling. This module uses several different regex queries to determine if there is a secret in your repository. To get a good overview of the results I used Pester the PowerShell testing framework to create tests around this secret scanner.
To use Pester you can create a test file in your repository. Let’s assume we have a repository structure like this:
Project
| README.md
| pipeline.yml
└───Settings
| └───Solution1
| | └───TST
| | | | Solution1.json
| | └───PROD
| | | Solution1.json
| └───Solution2
| | └───TST
| | | | Solution2.json
| | └───PROD
| | | Solution2.json
└───Solutions
| └───Solution1
| | └───Managed
| | | └───environmentvariabledefinitions
| | | | └───Var1
| | | | | Var1.xml
| | | | └───Var2
| | | | | Var2.xml
| | | └───Other
| | | | | Customizations.xml
| | | | | Solution.xml
| | | └───Workflows
| | | | | Flow1.json
| | | | | Flow1.json.data.xml
| └───Solution2
| | └───Managed
| | | └───environmentvariabledefinitions
| | | | └───Var1
| | | | | Var1.xml
| | | | └───Var2
| | | | | Var2.xml
| | | └───Other
| | | | | Customizations.xml
| | | | | Solution.xml
| | | └───Workflows
| | | | | Flow1.json
| | | | | Flow1.json.data.xml
└───Tests
| | Secrets.tests.ps1
It’s possible to also have a unmanaged version in the structure or to have more stages to deploy, but for simplicity sake I’ve limited it to this. The test I will provide will offer the option to expend it easily to more types.
The Secret.tests.ps1 file looks like this:
[string] $solution
)
BeforeDiscovery {
Install-Module PSSecretScanner -Force
$ScanTypes = @(
@{
type = "Settings"
subtypes = @(@{ subtype = "TST"},@{ subtype = "PRD"})
},
@{
type = "Solutions"
subtypes = @(@{ subtype = "Managed"})
}
)
}
Describe "Secrets in <type>" -ForEach $ScanTypes {
BeforeAll {
Import-Module PSSecretScanner
}
It " $solution Should not contain secrets in <SubType>" -ForEach $subtypes {
# Check for secrets
$path = Join-Path -Path $PSScriptRoot -ChildPath "..\$type\$solution\$subType\"
$secrets = Find-Secret -Path "$path"
$secrets.results | Foreach-Object {
"Problem in $($_.Filename) at line $($_.LineNumber) with pattern: $($_.PatternName)"
} | Should -BeNullOrEmpty
}
}
This test uses a parameter to provide the name of the solution, this way in the pipeline it’s possible to make a pipeline that will only deploy certain solutions and have the tests run for only those solutions.
In the beforediscovery part the settings and solutions are defined and their underlying folders. So if you have more folders you just add them here to the array.
The rest of the code is the test which will get all files in the folders and scans them for secrets. If a secret is found it will be added to an array with an easily readable text. So this way if a problem is found you can easily check the logs of the tests to see where the problem is.
Here is the pipeline.yml file
- none
pool:
vmImage: ubuntu-latest
parameters:
- name: solutions
type: stringList
displayName: Solutions to Deploy
values:
- Solution1
- Solution2
default:
- Solution1
- Solution2
jobs:
- job: Run_Tests
displayName: Run Tests over code
steps:
- checkout: self
- ${{ each solution in parameters.solutions }}:
- task: PowerShell@2
displayName: Run Tests for ${ }
inputs:
targetType: 'inline'
script: |
# Make sure Pester is installed
Install-Module Pester -Force
# Set up Pester Configuration
$configuration = New-PesterConfiguration
$configuration.Run.Path = "$(System.DefaultWorkingDirectory)/Tests/*.tests.ps1"
$configuration.TestResult.Enabled = $true
$configuration.TestResult.OutputPath = "$(System.DefaultWorkingDirectory)/${ }_testresults.xml"
$configuration.Run.PassThru = $true
$configuration.Output.Verbosity = 'Detailed'
# Set up the test container
$container = New-PesterContainer -Path "*.tests.ps1" -Data @{ solution = "${ }"}
$configuration.Run.Container = @($container)
# Run Pester
$PesterOutput = Invoke-Pester -Configuration $configuration
pwsh: true
continueOnError: true
condition: in(variables['Agent.JobStatus'], 'Succeeded', 'SucceededWithIssues', 'Failed')
- task: PublishTestResults@2
displayName: Publish TestResults
inputs:
testResultsFormat: "NUnit"
testResultsFiles: "*testresults.xml"
failTaskOnFailedTests: false
testRunTitle: 'Testresults'
condition: in(variables['Agent.JobStatus'], 'Succeeded', 'SucceededWithIssues', 'Failed')
This will create a pipeline with a dropdown box where you can select which solution to test. It’s using the fairly new StringList parameter type. The tasks are set up to continue even when failed so that if your test fail the results will still be published.
If you run the pipeline in Azure DevOps you will get a testreport like this
If you click the failed tests you will see an error message like:
Expected $null or empty, but got 'Problem in Solution1.json at line 91 with pattern: Password equal no quotes'.
This should help you track down the secrets in your environments.
