Continuous Security with OWASP ZAP and Azure DevOps (part 2)

In part 2 of a series on leveraging the OWASP ZAP Docker Image in Azure, this post describes how to utilise the ARM template described in Part 1, and embed it into an Azure DevOps pipeline as part of a continuous security regime.

Continuous Security with OWASP ZAP and Azure DevOps (part 2)

In a previous post I described how to deploy the OWASP ZAP Docker Image to Azure using my preferred IaC solution: ARM templates. This post describes how to leverage that ARM template by embedding it into an Azure DevOps pipeline, from where it can be used as an automated continuous security standard.

I started with a basic, empty Azure Release Pipeline. The steps below explain how to configure the required tasks as part of a deployment stage, but you may want to skip to the bottom to find a pre-configured Task Group that can easily be imported into your Azure DevOps instance.

Step 1 - Deploy container

With a release pipeline and empty deployment stage in place, the first task is to deploy the ARM template described in part 1. This is reasonably straightforward using the Azure resource group deployment task, and simply pointing it at the Git repository where the ARM template is defined:


For my pipeline I simply override the target parameter from the Azure parameters file defined in my Git repository, though if you want more configurability you could add other options too. The target subscription is also provided as a parameter, and Azure DevOps is smart enough to imply its type and provide the appropriate UI elements for the variable when you come to create a release.

Step 2 - Download report

With our ZAP container provisioned and a baseline run executed, we now need to extract the report. I modified step 1 to include the ARM template outputs as the variable owaspResult, which allows us to get hold of several properties that together describe the report location:

  • reportStorageAccount - The storage account name is a unique ID generated by the ARM template, so we need to get this in order to download the report.
  • reportSasToken - The SAS token for the storage account.
  • reportBlobContainer - The Blob container name. Hardcoded in the template, but we extract the container name from the output in case it gets refactored later.
  • reportFilename - The name of the file, also taken from the ARM outputs in case we change the way it is generated in the template.

We use some basic Azure PowerShell to download the blob content to the local working directory, and then run an XSL transform (thanks Francis Lacroix) to turn the output into the Nunit test format that Azure DevOps understands. I branched this into my Git repo, and refer to it directly over the web in the script.

$owasp = ConvertFrom-Json '$(owaspResult)'

$context = New-AzureStorageContext $owasp.reportStorageAccount.value -SasToken $owasp.reportSasToken.value

Get-AzureStorageBlobContent -Container $owasp.reportBlobContainer.value -Blob $owasp.reportFilename.value -Destination "$(System.DefaultWorkingDirectory)\owasp.xml" -Context $context -Force

$XslPath = ""
$XmlInputPath = "$(System.DefaultWorkingDirectory)\owasp.xml"
$XmlOutputPath = "$(System.DefaultWorkingDirectory)\owasp.testresults.xml"
$XslTransform = New-Object System.Xml.Xsl.XslCompiledTransform
$XslTransform.Transform($XmlInputPath, $XmlOutputPath)

This is run inline as part of an Azure Powershell task.


Step 3 - Publish results

Next we need to publish the results to Azure DevOps using the Publish test results task. This requires a few settings from previous tasks, including where to look for the output and what format it is in.


The result is an Azure DevOps test run containing the reported issues. You can generate Work Items from this just the same way that you would with other test failures.


Step 4 - Clean up

There's no point paying for resources that we're not using, so as we've now extracted the data we need from the ZAP run we can destroy the deployed Container Instance and Storage Account. This is super-easy, I decided to do it with inline Azure PowerShell so that we have a consistent scripting technology alongside step 2.

Remove-AzureRmResourceGroup -Name $(resourceGroup) -Force

I also set up this task's Control Options to run Even when the task fails, even if the deployment was cancelled. This ensures that a failure state won't leave us footing the bill for our Container Instance.


Task Groups

Finally, I wanted to make sure that I could reuse these steps for multiple targets. By selecting the steps and choosing "Create Task Group" it's possible to save this process as a reusable template within your Azure DevOps instance.

There's a minor issue with this as DevOps identifies the ARM output parameter as a required input for the group. This is easily remedied by pressing Export for the task group, deleting the parameter JSON definition in your favourite text editor, and then using Import to replace it.

The result is a reusable task sequence that enables your teams to quickly and easily add a pen test baseline to their continuous integration/deployment/delivery pipelines.


What's next? I'd like to package up this functionality to make it available on the Azure DevOps marketplace. In the meantime, please refer back to the repository for the latest version and instructions, including a JSON pipeline definition for if you don't fancy recreating this process from scratch:

The pipeline group is called az-devops-task-group.json.

Pipeline header image credit: Fabian Jung