Use PSRule for Azure with GitHub Actions

In a DevOps environment, incorporating testing into a continuous integration (CI) process is crucial. The integration within CI facilitates the early identification of issues before code deployment to any environment. While this practice widely adopted for application code, it is also important to apply this for infrastructure as code templates. In this post I want to explain what PSRule for Azure is, how I have implemented it for our company with GitHub Actions, and providing insights in the configurations that I have used.

What is PSRule for Azure?

PSRule for Azure makes it possible to implement security, best practices and organization standards into an infrastructure as code CI process. It basically checks and validates Infrastructure as Code for best practices and can also include custom policies to meet organizational standards and compliance requirements.

PSRule for Azure is built on top of PSRule (https://github.com/microsoft/PSRule) a cross-platform Powershell module with commands to test infrastructure code. PSRule is modular, allowing the creation of rules using Powershell code.

PSRule for Azure (https://github.com/Azure/PSRule.Rules.Azure) is a set of pre-built rules based on the principals of the Azure Well-Architected Framework, and can be used to test Bicep or ARM templates.

Let’s take for example this simple bicep code to deploy a storage account:

resource storageAccount 'Microsoft.Storage/storageAccounts@2021-06-01' = {
  name: 'project1-sa'
  location: 'West Europe'
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
}

While this is enough to deploy a storage account and will show no errors or issues when deploying, it doesn’t follow security best practices guided by Microsoft:

  • Storage firewall has not been enabled
  • Minimum TLS version has not been configured
  • Secure Transfer has not been configured
  • Soft delete has not been enabled

Deploying a storage account in this manner may pose a security risk, potentially resulting in a security incident and compromising the data that is stored in the storage account.

PSRule for Azure can validate if the Bicep code follows the best practices in a early stage, before deploying resources in a pipeline or during local development. This way you can make sure that the resources that are deployed are following the best practices and also adhere to compliancy rules. By integrating PSRule into the development process, potential issues can be identified and rectified before reaching the deployment stage, contributing to a more secure, compliant, and efficient infrastructure.

Configuration options file

Configuration options for PSRule for Azure are set in the configurations options file ps-rule.yaml. Please see the documentation for all configuration options:

This is a basic configuration options file that scans Bicep templates:

#
# PSRule for Azure configuration
#

# Configure binding for local rules.
binding:
  preferTargetInfo: true
  targetType:
    - type
    - resourceType

# Require minimum versions of modules.
requires:
  PSRule: "@pre >=2.9.0"
  PSRule.Rules.Azure: ">=1.29.0"

# Use PSRule for Azure.
include:
  module:
    - PSRule.Rules.Azure

execution:
  ruleSuppressed: Warn
  unprocessedObject: Debug

output:
  culture:
    - "en-US"

configuration:
  # Enable automatic expansion of Azure parameter files.
  AZURE_PARAMETER_FILE_EXPANSION: true

  # Enable Bicep CLI checks.
  AZURE_BICEP_CHECK_TOOL: true

  # Configures the number of seconds to wait for build Bicep files.
  AZURE_BICEP_FILE_EXPANSION_TIMEOUT: 60

rule:
  # Enable custom rules that don't exist in the baseline
  includeLocal: true

Value Purpose
PSRule: “@pre >=2.9.0” Specifies the module version contstraints for running PSRule. The module versions can be found here: PSRule releases
PSRule.Rules.Azure: “>=1.33.0” Specifies the module version contstraints for running PSRule for Azure. The module versions can be found here: PSRule.Rules.Azure releases
ruleSuppressed: Warn Determines how to handle suppressed rules: - None: No preference. Inherits the default of Warn. - Ignore: Continue to execute silently. - Warn: Continue to execute but log a warning. This is the default. - Error: Abort and throw an error. - Debug: Continue to execute but log a debug message.
unprocessedObject: Debug Determines how to report objects that are not processed by any rule: - None: No preference. Inherits the default of Warn. - Ignore: Continue to execute silently. - Warn: Continue to execute but log a warning. This is the default. - Error: Abort and throw an error. - Debug: Continue to execute but log a debug message.
AZURE_PARAMETER_FILE_EXPANSION This configuration option determines if Azure template parameter files will automatically be expanded.
AZURE_BICEP_CHECK_TOOL Enable checking the Bicep CLI version during initialization.
AZURE_BICEP_FILE_EXPANSION_TIMEOUT This configuration option determines the maximum time to spend building a single Bicep source file. The timeout is configured in seconds.
includeLocal Automatically include all local rules in the search path unless they have been explicitly excluded.

One important thing to note is to order to scan a Bicep template, a ARM parameter file must also be created for the template in the same directory. This is because a lot of our Bicep templates parameters do not have a default value, which stops the PSRule scan when a value cannot be found. Also in order to test our templates with different configurations, we can use different parameter files to scan for different configurations.

By adding the AZURE_PARAMETER_FILE_EXPANSION option, the parameter file will be expanded and linked to the the bicep template in the same directory and resolve parameters, variables and conditions.

Github Action

We are using PSRule for Azure for our centralized Bicep registry, which consists of a GIT repository containing all the templates that are published in an Azure Container registry that is consumed by other projects/teams.

The templates consists of reusable bicep modules, of which the parameters are loaded by providing a config.json parameter file, which has all the necessary parameters for each bicep template.

Each time a new module has been created or updated, a pull request will be created which trigger a GitHub Action workflow that looks for new or changed templates. It then creates a ARM parameter file in the same directory as the bicep template with the necessary values and scans the templates based on the configurations setup in ps-rule.yaml.

PSRuleforAzure

config.json

To streamline the development and testing of Bicep templates, we use a singular config.json. This configuration file contains all essential parameters for various templates (storage accounts, virtual network, aks etc.). It re-usable for every template created or updated, which allows us to quickly make changes and also promotes easy sharing among team members. This unified configuration enhances collaboration, accelerates development cycles, and ensures consistency across the team’s Bicep projects.

Steps

The Github Action steps for PSRule for Azure are:

Create ARM Parameter file

This step creates a main.parameters.json file in the same folder as the bicep template main.bicep file. An example for the storage account template:

├── azure
│   ├── storage-account
│   │   ├── bicep
│   │   │   ├── main.bicep
│   │   │   ├── main.parameters.json
│   │   │   ├── modules
│   │   │   │   ├── storage-account.bicep
│   │   │   │   ├── private-endpoint.bicep

To link the parameter file to the main bicep file, a metadata block is included within the parameter file. This is the powershell code to create a parameter file:

$config = Get-Content $deploymentConfigFile | ConvertFrom-Json
$newJson = [ordered]@{
  '$schema'      = 'https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#'
  contentVersion = "1.0.0.0"
  metadata       = @{template = './main.bicep'}
  parameters     = 
  @{
    'config' = @{'value' = $config }
  }
}

Set-Content -Path "main.parameters.json" -Value (ConvertTo-Json $newJson -Depth 100) -Confirm:$false

The metadata block is ignored by ARM. By specifying the metadata.template property, we can identify the specific template this parameter file refers to. The example above refers to a template file in the same directory as the parameter file by using “./”.

PSRule validation

For this, we are using the PSRule extension for Github Action in the YAML pipeline

name: "Execute PSRule module test"
description: "Execute PSRule module test"

inputs:
  moduleFolderPath:
    description: "The path to the module folder"
    required: true
    default: ""
  psrulePath:
    description: "The path to PSRule configurations"
    required: true


runs:
  using: "composite"
  steps:
    # [PSRule validation] task(s)
    #-----------------------------
    - name: Run PSRule analysis
      uses: microsoft/ps-rule@v2.9.0
      continue-on-error: true # Setting this whilst PSRule gets bedded in, in this project
      with:
        modules: "PSRule.Rules.Azure"
        inputPath: "${{ inputs.moduleFolderPath}}/"
        outputFormat: Csv
        outputPath: "${{ inputs.moduleFolderPath}}PSRule-output.csv"
        option: "${{ inputs.psrulePath}}/ps-rule.yaml" # Path to PSRule configuration options file
        source: "${{ inputs.psrulePath}}/.ps-rule/" # Path to folder containing suppression rules to use for analysis.
        summary: true

To output will show something like this: PSRuleGitHubAction

Bicepparam

PSRule can also use bicepparam files instead of JSON parameter files. The benefits for using bicepparam files is that JSON file lacks validation during creating or updating Bicep files. It doesn’t show when required entries are missing or that parameter values are invalid. A bicepparam file is using the same syntax as Bicep files and is much more compact then JSON parameter files. In the VSCode editor, it automatically shows warning for missing parameters and if the values match the restrictions applied in the bicep file.

Using bicepparam files in PSRule can be configured by setting the AZURE_BICEP_PARAMS_FILE_EXPANSION to true in the psrule.yaml file:

configuration:

  # Expand Bicep module from Bicep parameter files.
  AZURE_BICEP_PARAMS_FILE_EXPANSION: true

Local scan

The “PS Rule for Azure” engine consists of:

  • The PSRule (PSRule) & PSRule for Azure (PSRule.Rules.Azure) Powershell modules.
  • The configuration options file: ps-rule.yaml
  • (Optional) a .ps-rule subfolder for suppression groups.

The modules can be installed with the following commands:

Install-Module -Name 'PsRule' -Repository PSGallery -Scope CurrentUser -Force
Install-Module -Name 'PSRule.Rules.Azure' -Repository PSGallery -Scope CurrentUser -Force

Scanning a bicep template locally can be done with the following Powershell commands:

$moduleWorkingDirectory = Join-Path -Path $PSScriptRoot -ChildPath "..\storage-account\bicep\"
$psRuleConfigPath = Join-Path -Path $PSScriptRoot -ChildPath "..\psrule\"

Set-Location -Path $moduleWorkingDirectory

Assert-PSRule `
  -Format File `
  -InputPath ./ `
  -Module 'PSRule.Rules.Azure' `
  -Option $psRuleConfigPath `

Considering the following folder structure, where scan-bicepmodule.ps1 is used to scan bicep templates, the above commands will scan the storage account bicep templates (main and modules):

├── azure
│   ├── scripts
│   │   ├── scan-biceptemplates.ps1
│   ├── psrule
│   │   ├── ps-rule.yaml
│   │   ├── .ps-rule
│   ├── storage-account
│   │   ├── bicep
│   │   │   ├── main.bicep
│   │   │   ├── main.parameters.json
│   │   │   ├── modules
│   │   │   │   ├── storage-account.bicep
│   │   │   │   ├── private-endpoint.bicep

The output will look something like this:

PSRuleLocal

There is also a PSRule extension for VS Code, to integrate PSRule for Azure and run tests instantly when writing Bicep code. More information can be found here: PSRule-vscode

Suppressions

There are some situations that require to deviate from predefined rules. To do this, a suppression or exclusion from rules can be configured. There are 4 ways to suppress or exclude rules:

  • Exclude a rule to disable testing for all tested resources.
  • Suppress a rule to skip or ignore a rule for a specific case or exception.
  • Suppress a rule based on conditions with a suppression group.
  • Ignoring files

Exclude a rule

To exclude a rule, the Rule.Exclude option can be used in the ps-rule.yaml file.

rule:
  # Enable custom rules that don't exist in the baseline
  includeLocal: true
  exclude:
    # Ignore the following rules for all resources
    - Azure.Storage.ContainerSoftDelete

The above exclusion disables checking for ContainerSoftDelete for storage accounts.

Suppress a rule

To suppress a rule, the Suppression option can be used in the ps-rule.yaml file. A suppression rule can also include resources that will be suppressed.

suppression:
  Azure.Storage.ContainerSoftDelete:
  - project1-sa

The above suppression ignores checking for ContainerSoftDelete for the storage account project1-sa.

Suppression Group

A suppression group can be used to suppress rules based on conditions, for example to ignore resources with a specific tag.

To use a suppression group, a Suppression Group YAML definition must be created in the .ps-rule subfolder.

├── azure
│   ├── psrule
│   │   ├── ps-rule.yaml
│   │   ├── .ps-rule
│   │   │   ├── SuppressContainerSoftDelete.Rule.yaml
│   ├── storage-account
│   │   ├── bicep
│   │   │   ├── main.bicep
│   │   │   ├── main.parameters.json
│   │   │   ├── modules
│   │   │   │   ├── storage-account.bicep
│   │   │   │   ├── private-endpoint.bicep

For example:

---
# Synopsis: Suppress container soft delete for storage accounts
apiVersion: github.com/microsoft/PSRule/v1
kind: SuppressionGroup
metadata:
  name: Local.SuppressContainerSoftDeleteStorage
spec:
  rule:
  - Azure.Storage.ContainerSoftDelete
  if:
    field: tags.env
    equals: dev

The above suppression groups ignores checking for ContainerSoftDelete for storage account with the env:dev tags.

Ignore files

To exclude or ignore files from being processed, the Input.PathIgnore option can be used in the ps-rule.yaml file.

input:
  pathIgnore:
  # Exclude files with these extensions
  - '*.md'
  # Exclude specific configuration files
  - 'bicepconfig.json'

It is also possible to use only include specific files, for example only specific bicep test files:

input:
  pathIgnore:
  # Exclude all files.
  - "*"
  # Only process test files.
  - '!**/deploy.bicep'

More information about exceptions can be found here Suppressions

In conclusion, I hope this post provides valuable insights in how to use PSRule for Azure in the testing of centralized Bicep templates. I really like this shift-left capability and would highly recommend it, as it provides us to address potential issues early before deploying to Azure. This ensures that our templates align with Microsoft’s best practices and contributes to a more secure and efficient deployment process, and will make our internal and customers security teams a lot happier!