diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index 92e2173a..4fef76fd 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: InstallPester id: InstallPester shell: pwsh @@ -38,7 +38,7 @@ jobs: Write-Host "::debug:: RunPester $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" & './arm-ttk/GitHubWorkflow/Steps/RunPester.ps1' @Parameters - name: PublishTestResults - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: PesterResults path: '**.TestResults.xml' diff --git a/arm-ttk/arm-ttk.psd1 b/arm-ttk/arm-ttk.psd1 index ac41b0c7..850f7c95 100644 --- a/arm-ttk/arm-ttk.psd1 +++ b/arm-ttk/arm-ttk.psd1 @@ -1,5 +1,5 @@ @{ - ModuleVersion = 0.25 + ModuleVersion = 0.26 ModuleToProcess = 'arm-ttk.psm1' Description = 'Validation tools for Azure Resource Manager Templates' FormatsToProcess = 'arm-ttk.format.ps1xml' diff --git a/arm-ttk/testcases/deploymentTemplate/adminPassword-Should-Not-Be-A-Literal.test.ps1 b/arm-ttk/testcases/deploymentTemplate/adminPassword-Should-Not-Be-A-Literal.test.ps1 new file mode 100644 index 00000000..7341302f --- /dev/null +++ b/arm-ttk/testcases/deploymentTemplate/adminPassword-Should-Not-Be-A-Literal.test.ps1 @@ -0,0 +1,90 @@ +<# +.Synopsis + Ensures that all adminPasswords are expressions +.Description + Ensures that all properties within a template named adminPassword are expressions, not literal strings +#> +param( + [Parameter(Mandatory = $true)] + [PSObject] + $TemplateObject +) + +# Find all references to an adminPassword +# Filtering the complete $TemplateObject directly fails with "The script failed due to call depth overflow." errors +function Check-PasswordsInTemplate { + param ( + [Parameter(Mandatory = $true)] + [PSObject] + $TemplateObject, + [Parameter(Mandatory = $true)] + [string] + $AdminPwd + ) + if ("resources" -in $TemplateObject.PSobject.Properties.Name) { + $adminPwdRefsResources = $TemplateObject.resources | + Find-JsonContent -Key $AdminPwd -Value * -Like + + foreach ($ref in $adminPwdRefsResources) { + # Walk over each one + # if the property is not a string, then it's likely a param value for a nested deployment, and we should skip it. + $a = $ref.$AdminPwd + if ($a -isnot [string]) { + #check to see if this is a param value on a nested deployment - it will have a value property + if ($a.value -is [string]) { + $trimmedPwd = "$($a.value)".Trim() + } + else { + continue # since we don't know what object shape we're testing at this point (could be a param declaration on a nested deployment) + } + } + else { + $trimmedPwd = "$($a)".Trim() + } + if ($trimmedPwd -notmatch '\[[^\]]+\]') { + # If they aren't expressions + Write-Error -TargetObject $ref -Message "AdminPassword `"$trimmedPwd`" is not an expression" -ErrorId AdminPassword.Is.Literal # write an error + continue # and move onto the next + } + } + } + + if ("variables" -in $TemplateObject.PSobject.Properties.Name) { + $adminPwdRefsVariables = $TemplateObject.variables | + Find-JsonContent -Key $AdminPwd -Value * -Like + + foreach ($ref in $adminPwdRefsVariables) { + # Walk over each one + # if the property is not a string, then it's likely a param value for a nested deployment, and we should skip it. + if ($ref.$AdminPwd -isnot [string]) { continue } + $trimmedPwd = "$($ref.$AdminPwd)".Trim() + if ($trimmedPwd -notmatch '\[[^\]]+\]') { + # If they aren't expressions + Write-Error -TargetObject $ref -Message "AdminPassword `"$trimmedPwd`" is variable which is not an expression" -ErrorId AdminPassword.Var.Is.Literal # write an error + continue # and move onto the next + } + } + + # TODO - irregular doesn't handle null gracefully so we need to test for it + if ($trimmedPwd -ne $null) { + $PwdHasVariable = $trimmedPwd | ? -Extract + # this will return the outer most function in the expression + $PwdHasFunction = $trimmedPwd | ? -Extract + + # If we had a variable reference (not inside of another function) - then check it + # TODO this will not flag things like concat so we should add a blacklist here to ensure it's still not a static or deterministic password + if ($PwdHasVariable -and $PwdHasFunction.FunctionName -eq 'variables') { + $variableValue = $TemplateObject.variables.($PwdHasVariable.VariableName) + $variableValueExpression = $variableValue | ? + if (-not $variableValueExpression) { + Write-Error "AdminPassword references variable '$($PwdHasVariable.variableName)', which has a literal value. " -ErrorId AdminPassword.Is.Variable.Literal # write an error + } + } + } + } +} + +$pwdValues = @("administratorLoginPassword", "adminPassword") +foreach ($pwdValue in $pwdValues) { + Check-PasswordsInTemplate -TemplateObject $TemplateObject -AdminPwd $pwdValue +} diff --git a/arm-ttk/testcases/deploymentTemplate/adminUsername-Should-Not-Be-A-Literal.test.ps1 b/arm-ttk/testcases/deploymentTemplate/adminUsername-Should-Not-Be-A-Literal.test.ps1 index 1aa5abb6..ef6801e0 100644 --- a/arm-ttk/testcases/deploymentTemplate/adminUsername-Should-Not-Be-A-Literal.test.ps1 +++ b/arm-ttk/testcases/deploymentTemplate/adminUsername-Should-Not-Be-A-Literal.test.ps1 @@ -12,67 +12,79 @@ param( # Find all references to an adminUserName # Filtering the complete $TemplateObject directly fails with "The script failed due to call depth overflow." errors - -if ("resources" -in $TemplateObject.PSobject.Properties.Name) { - $adminUserNameRefsResources = $TemplateObject.resources | - Find-JsonContent -Key adminUsername -Value * -Like - - foreach ($ref in $adminUserNameRefsResources) { - # Walk over each one - # if the property is not a string, then it's likely a param value for a nested deployment, and we should skip it. - $a = $ref.adminUsername - if ($a -isnot [string]) { - #check to see if this is a param value on a nested deployment - it will have a value property - if ($a.value -is [string]) { - $trimmedUserName = "$($a.value)".Trim() +function Check-UsernamesInTemplate { + param ( + [Parameter(Mandatory = $true)] + [PSObject] + $TemplateObject, + [Parameter(Mandatory = $true)] + [string] + $AdminUsername + ) + if ("resources" -in $TemplateObject.PSobject.Properties.Name) { + $adminUserNameRefsResources = $TemplateObject.resources | + Find-JsonContent -Key $AdminUsername -Value * -Like + + foreach ($ref in $adminUserNameRefsResources) { + # Walk over each one + # if the property is not a string, then it's likely a param value for a nested deployment, and we should skip it. + $a = $ref.$AdminUsername + if ($a -isnot [string]) { + #check to see if this is a param value on a nested deployment - it will have a value property + if ($a.value -is [string]) { + $trimmedUserName = "$($a.value)".Trim() + } + else { + continue # since we don't know what object shape we're testing at this point (could be a param declaration on a nested deployment) + } } else { - continue # since we don't know what object shape we're testing at this point (could be a param declaration on a nested deployment) + $trimmedUserName = "$($a)".Trim() + } + if ($trimmedUserName -notmatch '\[[^\]]+\]') { + # If they aren't expressions + Write-Error -TargetObject $ref -Message "AdminUsername `"$trimmedUserName`" is not an expression" -ErrorId AdminUsername.Is.Literal # write an error + continue # and move onto the next } - } - else { - $trimmedUserName = "$($a)".Trim() - } - if ($trimmedUserName -notmatch '\[[^\]]+\]') { - # If they aren't expressions - Write-Error -TargetObject $ref -Message "AdminUsername `"$trimmedUserName`" is not an expression" -ErrorId AdminUsername.Is.Literal # write an error - continue # and move onto the next } } -} - -if ("variables" -in $TemplateObject.PSobject.Properties.Name) { - $adminUserNameRefsVariables = $TemplateObject.variables | - Find-JsonContent -Key adminUsername -Value * -Like - - foreach ($ref in $adminUserNameRefsVariables) { - # Walk over each one - # if the property is not a string, then it's likely a param value for a nested deployment, and we should skip it. - if ($ref.adminUserName -isnot [string]) { continue } - $trimmedUserName = "$($ref.adminUserName)".Trim() - if ($trimmedUserName -notmatch '\[[^\]]+\]') { - # If they aren't expressions - Write-Error -TargetObject $ref -Message "AdminUsername `"$trimmedUserName`" is variable which is not an expression" -ErrorId AdminUsername.Var.Is.Literal # write an error - continue # and move onto the next + + if ("variables" -in $TemplateObject.PSobject.Properties.Name) { + $adminUserNameRefsVariables = $TemplateObject.variables | + Find-JsonContent -Key $AdminUsername -Value * -Like + + foreach ($ref in $adminUserNameRefsVariables) { + # Walk over each one + # if the property is not a string, then it's likely a param value for a nested deployment, and we should skip it. + if ($ref.$AdminUsername -isnot [string]) { continue } + $trimmedUserName = "$($ref.$AdminUsername)".Trim() + if ($trimmedUserName -notmatch '\[[^\]]+\]') { + # If they aren't expressions + Write-Error -TargetObject $ref -Message "AdminUsername `"$trimmedUserName`" is variable which is not an expression" -ErrorId AdminUsername.Var.Is.Literal # write an error + continue # and move onto the next + } } - } - - # TODO - irregular doesn't handle null gracefully so we need to test for it - if ($trimmedUserName -ne $null) { - $UserNameHasVariable = $trimmedUserName | ? -Extract - # this will return the outer most function in the expression - $userNameHasFunction = $trimmedUserName | ? -Extract - - # If we had a variable reference (not inside of another function) - then check it - # TODO this will not flag things like concat so we should add a blacklist here to ensure it's still not a static or deterministic username - if ($UserNameHasVariable -and $userNameHasFunction.FunctionName -eq 'variables') { - $variableValue = $TemplateObject.variables.($UserNameHasVariable.VariableName) - $variableValueExpression = $variableValue | ? - if (-not $variableValueExpression) { - Write-Error @" -AdminUsername references variable '$($UserNameHasVariable.variableName)', which has a literal value. -"@ -ErrorId AdminUserName.Is.Variable.Literal # write an error + + # TODO - irregular doesn't handle null gracefully so we need to test for it + if ($trimmedUserName -ne $null) { + $UserNameHasVariable = $trimmedUserName | ? -Extract + # this will return the outer most function in the expression + $userNameHasFunction = $trimmedUserName | ? -Extract + + # If we had a variable reference (not inside of another function) - then check it + # TODO this will not flag things like concat so we should add a blacklist here to ensure it's still not a static or deterministic username + if ($UserNameHasVariable -and $userNameHasFunction.FunctionName -eq 'variables') { + $variableValue = $TemplateObject.variables.($UserNameHasVariable.VariableName) + $variableValueExpression = $variableValue | ? + if (-not $variableValueExpression) { + Write-Error "AdminUsername references variable '$($UserNameHasVariable.variableName)', which has a literal value. " -ErrorId AdminUserName.Is.Variable.Literal # write an error + } } } } } + +$usernameValues = @("administratorLogin", "adminUsername") +foreach ($usernameValue in $usernameValues) { + Check-UsernamesInTemplate -TemplateObject $TemplateObject -AdminUsername $usernameValue +} diff --git a/unit-tests/adminPassword-Should-Not-Be-A-Literal/Fail/Literal-Admin-AdminPassword.json b/unit-tests/adminPassword-Should-Not-Be-A-Literal/Fail/Literal-Admin-AdminPassword.json new file mode 100644 index 00000000..5d94d454 --- /dev/null +++ b/unit-tests/adminPassword-Should-Not-Be-A-Literal/Fail/Literal-Admin-AdminPassword.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "variables": { + "administratorLogin": "fixedusername", + "administratorLoginPassword": "fixedpassword" + }, + "resources": [ + { + "type": "Microsoft.DBforMySQL/flexibleServers", + "apiVersion": "2021-05-01", + "name": "name", + "location": "location", + "properties": { + "administratorLogin": "[variables('administratorLogin')]", + "administratorLoginPassword": "[variables('administratorLoginPassword')]" + } + } + ] +} \ No newline at end of file diff --git a/unit-tests/adminPassword-Should-Not-Be-A-Literal/Pass/ExpressionValue.json b/unit-tests/adminPassword-Should-Not-Be-A-Literal/Pass/ExpressionValue.json new file mode 100644 index 00000000..020a3ec8 --- /dev/null +++ b/unit-tests/adminPassword-Should-Not-Be-A-Literal/Pass/ExpressionValue.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "variables": { + "deploymentName": "Deployment-1.0" + }, + "resources": [ + { + "properties": { + "parameters": { + "adminPassword": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', variables('deploymentName')), '2019-10-01').outputs.accountSettings.value.accountAdminName]" + } + } + } + } + ] +} \ No newline at end of file diff --git a/unit-tests/adminPassword-Should-Not-Be-A-Literal/adminPassword-Should-Not-Be-A-Literal.tests.ps1 b/unit-tests/adminPassword-Should-Not-Be-A-Literal/adminPassword-Should-Not-Be-A-Literal.tests.ps1 new file mode 100644 index 00000000..d9e0b2ec --- /dev/null +++ b/unit-tests/adminPassword-Should-Not-Be-A-Literal/adminPassword-Should-Not-Be-A-Literal.tests.ps1 @@ -0,0 +1,6 @@ + +#requires -module arm-ttk +. $PSScriptRoot\..\arm-ttk.test.functions.ps1 +Test-TTK $psScriptRoot +return + diff --git a/unit-tests/adminUsername-Should-Not-Be-A-Literal/Fail/Literal-Admin-AdminLogin.json b/unit-tests/adminUsername-Should-Not-Be-A-Literal/Fail/Literal-Admin-AdminLogin.json new file mode 100644 index 00000000..5d94d454 --- /dev/null +++ b/unit-tests/adminUsername-Should-Not-Be-A-Literal/Fail/Literal-Admin-AdminLogin.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "variables": { + "administratorLogin": "fixedusername", + "administratorLoginPassword": "fixedpassword" + }, + "resources": [ + { + "type": "Microsoft.DBforMySQL/flexibleServers", + "apiVersion": "2021-05-01", + "name": "name", + "location": "location", + "properties": { + "administratorLogin": "[variables('administratorLogin')]", + "administratorLoginPassword": "[variables('administratorLoginPassword')]" + } + } + ] +} \ No newline at end of file