testcases/deploymentTemplate/Outputs-Must-Not-Contain-Secrets.test.ps1

<#
.Synopsis
    Ensures outputs do not contain secrets.
.Description
    Ensures outputs do not contain expressions that would expose secrets, list*() functions or secure parameters.
.Example
    Test-AzTemplate -TemplatePath .\100-marketplace-sample\ -Test Outputs-Must-Not-Contain-Secrets
.Example
    .\IDs-Should-Be-Derived-From-ResourceIDs.test.ps1 -TemplateObject (Get-Content ..\..\..\unit-tests\IOutputs-Must-Not-Contain-Secrets.test.json -Raw | ConvertFrom-Json)
#>

param(
[Parameter(Mandatory=$true,Position=0)]
[PSObject]
$TemplateObject
)

<#
This test should flag using runtime functions that list secrets or secure parameters in the outputs

    "sample-output": {
      "type": "string",
      "value": "[listKeys(parameters('storageAccountName'),'2017-10-01').keys[0].value]"
    }
    "sample-output-secure-param": {
      "type": "string",
      "value": "[concat('connectstring stuff', parameters('adminPassword'))]"
    }

#>


    $isListFunc = [Regex]::new(@'
(?> # we don't want to flag a UDF that might be called "myListOfIps" so we need to check the char preceeding list*()
    \[| # bracket
    \(| # paren
    , # comma
) # and the (?> ) syntax says this is not included in the match because we need to check for expressions explicitly below
\s{0,}
list\w{1,}
\s{0,}
\(
'@
, 'Multiline,IgnoreCase,IgnorePatternWhitespace')

$exprStrOrQuote = [Regex]::new('(?<!\\)[\[\"]', 'RightToLeft')

#look at each output value property
foreach ($output in $TemplateObject.outputs.psobject.properties) {

    $outputText = $output.value | ConvertTo-Json # search the entire output object to cover output copy scenarios
    if ($isListFunc.IsMatch($outputText)) {
        
        foreach ($m in $isListFunc.Matches($outputText)) {
            # Go back and find if it starts with a [ or a "
            $preceededBy = $exprStrOrQuote.Match($outputText, $m.Index + 1) # add 1 to index since the match has to include "list" plus at least one other char
            if ($preceededBy.Value -eq '[') {  # If it starts with a [, it's a real ref
                Write-Error -Message "Output contains secret: $($output.Name)" -ErrorId Output.Contains.Secret -TargetObject $output   
            }
        }
    }
    if ($output.Name -like "*password*"){
        Write-Error -Message "Output name suggests secret: $($output.Name)" -ErrorId Output.Contains.Secret.Name -TargetObject $output
    }
}

# find all secureString and secureObject parameters
foreach ($parameterProp in $templateObject.parameters.psobject.properties) {
    $parameter = $parameterProp.Value
    $name = $parameterProp.Name
    # If the parameter is a secureString or secureObject it shouldn't be in the outputs:
    if ($parameter.Type -eq 'securestring' -or $parameter.Type -eq 'secureobject') { 

        # Create a Regex to find the parameter
        $findParam = [Regex]::new(@"
parameters # the parameters keyword
\s{0,} # optional whitespace
\( # opening parenthesis
\s{0,} # more optional whitespace
' # a single quote
$name # the parameter name
' # a single quote
\s{0,} # more optional whitespace
\) # closing parenthesis
"@
,
    # The Regex needs to be case-insensitive
'Multiline,IgnoreCase,IgnorePatternWhitespace'
)
        
        foreach ($output in $TemplateObject.outputs.psobject.properties) {

            $outputText = $output.Value | ConvertTo-Json -Depth 100
            $outputText = $outputText -replace # and replace
                '\\u0027', "'" # unicode-single quotes with single quotes (in case we are not on core).

            $matched = $($findParam.Match($outputText))
            if ($matched.Success) {
                
                $matchIndex = $findParam.Match($outputText).Index
                $preceededBy = $exprStrOrQuote.Match($outputText, $matchIndex).Value
                if ($preceededBy -eq '[') {
                    Write-Error -Message "Output contains $($parameterProp.Value.Type) parameter: $($output.Name)" -ErrorId Output.Contains.SecureParameter -TargetObject $output
                }
            }
        }        
    }
}