Just show me the code
As always if you don’t care about the post I have upload the source code on my Github.

A big part of any organization’s risk assessment process is to be aware of and gain visibility into vulnerabilities in the software being used. Vulnerability scanning allows us to review the security state of the container images and take actions to fix issues identified during the scan, resulting in more secure deployments.

One of the main challenges a development team face is how to manage container security risk without slowing down application delivery. A way to address this early is by adopting a Secure DevOps workflow.
Image scanning is a key function of a Secure DevOps workflow. Image scanning refers to the process of analyzing the contents of a container image in order to detect security issues, vulnerabilities or bad practices.

container-img-scanning-workflow

Image scanning is easy to implement and automate and in this post I will be covering how you can use some of the most well-known scanners alongside with your Azure DevOps CI/CD YAML Pipelines.

Right now on the market there are a ton of great choices when you want to use a container vulnerability scanner. Snyk, Clair, Qualys, Sysdig or Trivy are some of the most well-known.

In this post I’ll be focusing on those 4 image scanners:

  • AWS Elastic Container Registry integrated scanner featuring Clair.
  • Azure Defender for ACR scan featuring Qualys.
  • Sysdig
  • AquaSec Trivy.

1- AWS Elastic Container Registry integrated scanner featuring Clair

Elastic Container Register (ECR) is a fully managed container registry offered by AWS, it uses the Common Vulnerabilities and Exposures (CVEs) database from the open-source Clair project.

You can manually scan container images stored in ECR or you can configure your repositories to scan images when you push them to a repository, that might be an issue for some companies because in a secure DevOps workflow you might want to scan the image BEFORE pushing it into the registry. In that case you have 2 options available:

  • Option 1: Run another security scanner like Trivy or Sysdig before pushing the image to ECR. If you’re interested in this option, go to section 3 or section 4.
  • Option 2: After pushing the image, get the security report from ECR and if there are any vulnerabilities delete the image.

Here’s a code snippet showing how you can achieve “Option 2” using Azure YAML Pipelines:

trigger:
  branches:
    include:
    - master
  paths:
    exclude:
    - pipelines/*
    - test/*
    - README.md
    - .dockerignore
    - .gitignore
 
variables:
- name: appName
  value: 'ScanApp'
- name: tag
  value: '$(Build.BuildId)' 
- name: awsCredentials
  value: 'aws-dev' 
- name: awsRegion
  value: 'eu-west-1' 
- name: awsAccount
  value: '9433277812'

pool:
  vmImage: 'ubuntu-latest'

steps:
- task: Bash@3
  displayName: Create application image
  inputs:
    targetType: 'inline'
    script: |
      docker build -t  ${{ lower(variables.appName) }}:$(tag) .      
    workingDirectory: '$(System.DefaultWorkingDirectory)'

- task: AWSShellScript@1
  displayName: Create ECR repository
  inputs:
    awsCredentials: '$(awsCredentials)'
    regionName: '$(awsRegion)'
    scriptType: 'inline'
    inlineScript: |
      aws ecr describe-repositories --repository-names ${{ lower(variables.appName) }} || aws ecr create-repository --repository-name ${{ lower(variables.appName) }} --image-scanning-configuration scanOnPush=true      
- task: AWSShellScript@1
  displayName: Push image to ECR repository
  inputs:
    awsCredentials: '$(awsCredentials)'
    regionName: '$(awsRegion)'
    scriptType: 'inline'
    inlineScript: |
      aws ecr get-login-password --region $(awsRegion) | docker login --username AWS --password-stdin $(awsAccount).dkr.ecr.$(awsRegion).amazonaws.com
      docker tag ${{ lower(variables.appName) }}:$(tag)  $(awsAccount).dkr.ecr.$(awsRegion).amazonaws.com/${{ lower(variables.appName) }}:$(tag)
      docker push $(awsAccount).dkr.ecr.$(awsRegion).amazonaws.com/${{ lower(variables.appName) }}:$(tag)      
   
- task: AWSShellScript@1
  displayName: Check security vulnerabilities
  inputs:
    awsCredentials: '$(awsCredentials)'
    regionName: '$(awsRegion)'
    scriptType: 'inline'
    inlineScript: |
      aws ecr wait image-scan-complete --repository-name ${{ lower(variables.appName) }} --image-id imageTag=$(tag)
      SCAN_FINDINGS=$(aws ecr describe-image-scan-findings --repository-name ${{ lower(variables.appName) }} --image-id imageTag=$(tag) | jq '.imageScanFindings.findingSeverityCounts')      
      CRITICAL=$(echo $SCAN_FINDINGS | jq '.CRITICAL')
      HIGH=$(echo $SCAN_FINDINGS | jq '.HIGH')
      MEDIUM=$(echo $SCAN_FINDINGS | jq '.MEDIUM')
      LOW=$(echo $SCAN_FINDINGS | jq '.LOW')
      INFORMATIONAL=$(echo $SCAN_FINDINGS | jq '.INFORMATIONAL')
      UNDEFINED=$(echo $SCAN_FINDINGS | jq '.UNDEFINED')
      if [ $CRITICAL != null ]; then
        echo Docker image contains vulnerabilities at CRITICAL level. The image will be deleted.
        aws ecr batch-delete-image --repository-name ${{ lower(variables.appName) }} --image-ids imageTag=$(tag)
        exit 1;
      fi
      if [ $HIGH != null ]; then
        echo Docker image contains vulnerabilities at HIGH level. The image will be deleted.
        aws ecr batch-delete-image --repository-name ${{ lower(variables.appName) }} --image-ids imageTag=$(tag)
        exit 1;
      fi
      if [ $MEDIUM != null || $LOW != null || $INFORMATIONAL != null ]; then
        echo Docker image contains medium or lower vulnerabilities.
      fi      

Let’s review what the pipeline is doing.

  • Create the image of your app.
- task: Bash@3
  displayName: Create application image
  inputs:
    targetType: 'inline'
    script: |
      docker build -t  ${{ lower(variables.appName) }}:$(tag) .      
    workingDirectory: '$(System.DefaultWorkingDirectory)'
  • Create the ECR repository if it doesn’t exist.

When creating the ECR repository it is very important setting the attribute scanOnPush=true.
If scanOnPush is enabled, images are scanned after being pushed. If scanOnPush is disabled on a repository, then you must manually start each image scan to get the scan results.

- task: AWSShellScript@1
  displayName: Create ECR repository
  inputs:
    awsCredentials: '$(awsCredentials)'
    regionName: '$(awsRegion)'
    scriptType: 'inline'
    inlineScript: |
      aws ecr describe-repositories --repository-names ${{ lower(variables.appName) }} || aws ecr create-repository --repository-name ${{ lower(variables.appName) }} --image-scanning-configuration scanOnPush=true      
  • Push the image to ECR
- task: AWSShellScript@1
  displayName: Push image to ECR repository
  inputs:
    awsCredentials: '$(awsCredentials)'
    regionName: '$(awsRegion)'
    scriptType: 'inline'
    inlineScript: |
      aws ecr get-login-password --region $(awsRegion) | docker login --username AWS --password-stdin $(awsAccount).dkr.ecr.$(awsRegion).amazonaws.com
      docker tag ${{ lower(variables.appName) }}:$(tag)  $(awsAccount).dkr.ecr.$(awsRegion).amazonaws.com/${{ lower(variables.appName) }}:$(tag)
      docker push $(awsAccount).dkr.ecr.$(awsRegion).amazonaws.com/${{ lower(variables.appName) }}:$(tag)      
  • Analyze the scan results.

The aws ecr wait image-scan-complete command waits until the image scan is complete and findings can be accessed, it will poll every 5 seconds until a successful state has been reached or will exit with a return code of 255 after 60 failed checks.

After retrieving and parsing the scanner results, the script checks for HIGH or CRITICAL vulnerabilities and if there are any, it deletes the image from ECR and breaks the pipeline execution.
If the scripts finds only MEDIUM, LOW or INFORMATIONAL vulnerabilities continues the execution.

I didn’t write the script used to check the vulnerabilities, I’m using a script from Sahan Ruwanga Gunathilaka post, so kudos to him.

- task: AWSShellScript@1
  displayName: Check security vulnerabilities
  inputs:
    awsCredentials: '$(awsCredentials)'
    regionName: '$(awsRegion)'
    scriptType: 'inline'
    inlineScript: |
      aws ecr wait image-scan-complete --repository-name ${{ lower(variables.appName) }} --image-id imageTag=$(tag)
      SCAN_FINDINGS=$(aws ecr describe-image-scan-findings --repository-name ${{ lower(variables.appName) }} --image-id imageTag=$(tag) | jq '.imageScanFindings.findingSeverityCounts')      
      CRITICAL=$(echo $SCAN_FINDINGS | jq '.CRITICAL')
      HIGH=$(echo $SCAN_FINDINGS | jq '.HIGH')
      MEDIUM=$(echo $SCAN_FINDINGS | jq '.MEDIUM')
      LOW=$(echo $SCAN_FINDINGS | jq '.LOW')
      INFORMATIONAL=$(echo $SCAN_FINDINGS | jq '.INFORMATIONAL')
      UNDEFINED=$(echo $SCAN_FINDINGS | jq '.UNDEFINED')
      if [ $CRITICAL != null ]; then
        echo Docker image contains vulnerabilities at CRITICAL level. The image will be deleted.
        aws ecr batch-delete-image --repository-name ${{ lower(variables.appName) }} --image-ids imageTag=$(tag)
        exit 1;
      fi
      if [ $HIGH != null ]; then
        echo Docker image contains vulnerabilities at HIGH level. The image will be deleted.
        aws ecr batch-delete-image --repository-name ${{ lower(variables.appName) }} --image-ids imageTag=$(tag)
        exit 1;
      fi
      if [ $MEDIUM != null || $LOW != null || $INFORMATIONAL != null ]; then
        echo Docker image contains medium or lower vulnerabilities.
      fi      

2- Azure Defender for ACR scan featuring Qualys

Azure Container Registry (ACR) is a managed, private Docker registry service for storing container images.

If you plan to use the image vulnerability scanning capabilities, you’ll need to enable Azure Defender for container registries at the subscription level. The scanner is powered by Qualys.

Azure Defender works exactly like AWS ECR, that means that it will only scan the images when they’re pushed to the registry.

With AWS ECR we had to build a script that fetch and analyze the scanning results, but in this case Microsoft has already built it, so you can just copy-paste it and you are good to go.
You can find it in the following link

I grabbed the Azure Pipeline and the script built by Microsoft and made some slight modifications.
Here’s how it looks:

trigger:
  branches:
    include:
    - master
  paths:
    exclude:
    - pipelines/*
    - test/*
    - README.md
    - .dockerignore
    - .gitignore
 
pool:
  vmImage: "ubuntu-latest"

variables:
  - name: registryName
    value: 'acrcponsndev'
  - name: imageName
    value: 'app-example-vulnerabilities'
  - name: azureSubscription
    value: 'service-endpoint-cpons-dev'
  - name: tag
    value: '$(Build.BuildId)'
  - name: waitForScanResultsAfterPushInMinutes
    value: '5'

stages:
- stage: Build_and_Publish
  displayName: Build and push image to registry than check for vulnerability scan results to determine publishing health
  jobs:
  - job: BuildAndPush
    displayName: BuildAndPush
    pool:
      vmImage: 'ubuntu-latest'
    steps:
    - task: AzureCLI@2
      displayName: AZ ACR Login
      inputs:
        azureSubscription: $(azureSubscription)
        scriptType: 'bash'
        scriptLocation: 'inlineScript'
        inlineScript: 'az acr login --name $(registryName)'
    - task: AzureCLI@2
      displayName: AZ ACR Build
      inputs:
        azureSubscription: $(azureSubscription)
        scriptType: 'bash'
        scriptLocation: 'inlineScript'
        inlineScript: 'az acr build -t $(imageName):$(tag) -t $(imageName):latest -r $(registryName) -f Dockerfile .'
        useGlobalConfig: true
        workingDirectory: '$(Build.SourcesDirectory)'

  - job: WaitForScanResults
    displayName: Wait for scan results
    pool: 'Server'
    dependsOn: 
    - BuildAndPush
    steps:
    - task: Delay@1
      inputs:
        delayForMinutes: '$(waitForScanResultsAfterPushInMinutes)'

  - job: ImageScanCheck
    displayName: Check for image vulnerabilities
    pool:
      vmImage: 'ubuntu-latest'
    dependsOn: 
    - BuildAndPush
    - WaitForScanResults
    steps:
      - task: AzureCLI@2
        inputs:
          azureSubscription: $(azureSubscription)
          scriptType: 'pscore'
          scriptLocation: 'scriptPath'
          scriptPath: '$(Build.SourcesDirectory)/acr-image-scan.ps1'
          arguments: '-registryName $(registryName) -repository $(imageName) -tag $(tag)'

This pipeline is using multiple jobs because the Delay@1 task is an agentless task and on a single job you can’t mix steps that requires an agent with step that don’t.

Let’s make a quick review about the steps involved in this pipeline.

  • Build and push the image into ACR.

The az acr build command creates a container image, tags it, and pushes it to the registry. It does all those steps with just a single command.
To be able to push the image to ACR we need to log in first, and that’s exactly what we’re doing with the az acr login command.

  - job: BuildAndPush
    displayName: BuildAndPush
    pool:
      vmImage: 'ubuntu-latest'
    steps:
    - task: AzureCLI@2
      displayName: AZ ACR Login
      inputs:
        azureSubscription: $(azureSubscription)
        scriptType: 'bash'
        scriptLocation: 'inlineScript'
        inlineScript: 'az acr login --name $(registryName)'
    - task: AzureCLI@2
      displayName: AZ ACR Build
      inputs:
        azureSubscription: $(azureSubscription)
        scriptType: 'bash'
        scriptLocation: 'inlineScript'
        inlineScript: 'az acr build -t $(imageName):$(tag) -t $(imageName):latest -r $(registryName) -f Dockerfile .'
        useGlobalConfig: true
        workingDirectory: '$(Build.SourcesDirectory)'
  • Stop the pipeline a fixed period of time.

The delay task is stopping the pipeline execution a fixed amount of time because the vulnerability scan takes a few minutes to complete. This step is needed to ensure that the results are available and we can can continue on.

  - job: WaitForScanResults
    displayName: Wait for scan results
    pool: 'Server'
    dependsOn: 
    - BuildAndPush
    steps:
    - task: Delay@1
      inputs:
        delayForMinutes: '$(waitForScanResultsAfterPushInMinutes)'
  • Get the scan results and analyze them

Invoke the script that fetches the scan result and assess the vulnerabilities.

  - job: ImageScanCheck
    displayName: Check for image vulnerabilities
    pool:
      vmImage: 'ubuntu-latest'
    dependsOn: 
    - BuildAndPush
    - WaitForScanResults
    steps:
      - task: AzureCLI@2
        inputs:
          azureSubscription: $(azureSubscription)
          scriptType: 'pscore'
          scriptLocation: 'scriptPath'
          scriptPath: '$(Build.SourcesDirectory)/acr-image-scan.ps1'
          arguments: '-registryName $(registryName) -repository $(imageName) -tag $(tag)'

I decided to not touch this script at all, because it is good enough as it is. But, let’s review it out anyways.

<#  
.SYNOPSIS  
  Automation script to include ASC vulnerability assessment scan summary for provided image as a gate. 
  Check result and assess whether to pass security gate by findings severity.
     
.DESCRIPTION  
  Azure secuirty center (ASC) scan Azure container registry (ACR) images for known vulnerabilities on multiple scenarios including image push. 
 (https://docs.microsoft.com/en-us/azure/security-center/defender-for-container-registries-introduction#when-are-images-scanned)  
  Using this tool you can have a security gate as part of image release(push) or 
  deployment to cluster to check if for image there is existent scan in ASC (for example part following push) - retry ,
  And assess it's findings by configurable thresholds to decide whether to fail the gate - i.e the release/deployment.
  Script extracts provided ACR image digest, try to extract ASC scan summary using Azure resource graph AZ CLI module and check if result is healthy or not.
  If Healthy, will exit gate successfully, otherwise if unhealthy, 
  Check for any high findings or medium findings count suppress their thresholds to fail the gate, otherwise will set gate in warning mode.
  In case there's a major vulnerability in image, gate(script) will fail to allow exit in CI/CD pipelines.
  
  
.PARAMETER registryName
  [mandatory] 
  Azure container registry resource name image is stored.
.PARAMETER repository
  [mandatory] 
  It can be any EXISTING resource group, using the ASC default "DefaultResourceGroup-XXX" is one option.
  Note: Since the ASC VA solution is not an Azure resource it will not be listed under the resource group, but still it is attached to it.
.PARAMETER tag
  [mandatory] 
  The name of the new solution
.PARAMETER scanExtractionRetryCount
  [optional] 
  Max retries to get image scan summary from ASC. (Useful for waiting for scan result to finish following image push).
.PARAMETER mediumFindingsCountFailThreshold
  [optional] 
  Threshold to fail gate on Medium severity findings count in scan (default is 5)
  ** In the case of High servirty finging gate will always fail.**
.PARAMETER lowFindingsCountFailThreshold
  [optional] 
  Threshold to fail gate on Low severity findings count in scan (default is 15)
  ** In the case of High servirty finging gate will always fail.**
  
  
.EXAMPLE
	.\ImageScanSummaryAssessmentGate.ps1 -registryName <registryResourceName> -repository <respository> -tag <tag>
.EXAMPLE
	.\ImageScanSummaryAssessmentGate.ps1 -registryName tomerregistry -repository build -tag latest
 
.NOTES
   AUTHOR: Tomer Weinberger - Software Engineer at Microsoft Azure Security Center
#>

# Prerequisites
# Azure CLI installed
# Optional: Azure CLI Resource Grpah extension installed (installed as part of script)
Param(
	# Image registry name
	[Parameter(Mandatory=$true)]
	$registryName,
	
	# Image repository name in registry
   	[Parameter(Mandatory=$true)]
	$repository,
	
	# Image tag
   	[Parameter(Mandatory=$true)]
	$tag,
	
	# Max retries to get image scan summary from ASC.
	$scanExtractionRetryCount = 3,
	
	# Medium servrity findings failure threshold
	$mediumFindingsCountFailThreshold = 5,
	
	# Low servrity findings failure threshold
	$lowFindingsCountFailThreshold = 15
)

az extension add --name resource-graph -y

$imageDigest = az acr repository show -n $registryName --image "$($repository):$($tag)" -o tsv --query digest
if(!$imageDigest)
{
	Write-Error "Image '$($repository):$($tag)' was not found! (Registry: $registryName)"
	exit 1
}

Write-Host "Image Digest: $imageDigest"

# All images scan summary ARG query.
$query = "securityresources
 | where type == 'microsoft.security/assessments/subassessments'
 | where id matches regex  '(.+?)/providers/Microsoft.ContainerRegistry/registries/(.+)/providers/Microsoft.Security/assessments/dbd0cb49-b563-45e7-9724-889e799fa648/'
 | extend registryResourceId = tostring(split(id, '/providers/Microsoft.Security/assessments/')[0])
 | extend registryResourceName = tostring(split(registryResourceId, '/providers/Microsoft.ContainerRegistry/registries/')[1])
 | extend imageDigest = tostring(properties.additionalData.imageDigest)
 | extend repository = tostring(properties.additionalData.repositoryName)
 | extend scanFindingSeverity = tostring(properties.status.severity), scanStatus = tostring(properties.status.code)
 | summarize scanFindingSeverityCount = count() by scanFindingSeverity, scanStatus, registryResourceId, registryResourceName, repository, imageDigest
 | summarize  severitySummary = make_bag(pack(scanFindingSeverity, scanFindingSeverityCount)) by registryResourceId, registryResourceName, repository, imageDigest, scanStatus"
 
# Add filter to get scan summary for specific provided image
$filter = "| where imageDigest =~ '$imagedigest' and repository =~ '$repository' and registryResourceName =~ '$registryname'" 
$query  = @($query, $filter) | out-string 

Write-Host "Query: $query"

# Remove query's new line to use ARG CLI
$query = $query -replace [Environment]::NewLine,"" -replace "`r`n","" -replace "`n",""

# Get result wit retry policy
$i = 0
while(($result = az graph query -q $query -o json | ConvertFrom-Json).count -eq 0 -and ($i = $i + 1) -lt $scanExtractionRetryCount)
{ 
	Write-Host "No results for image $($repository):$($tag) yet ..."
	Start-Sleep -s 20
}

if(!$result -or $result.count -eq 0)
{
	Write-Error "No results were found for digest: $imageDigest after $scanExtractionRetryCount retries!"
	exit 1
}

# # Extract scan summary from result
$scansummary = $result.data[0]
Write-Host "Scan summary: $($scansummary | out-string)"

if($scansummary.scanstatus -eq "healthy")
{
  Write-Host "Healthy scan result, no major vulnerabilities  found in image"
}
elseif($scansummary.scanstatus -eq "unhealthy")
{
	# Check if there are major vulnerabilities  found - customize by parameters
	if($scansummary.severitysummary.high -gt 0 `
	-or $scansummary.severitysummary.medium -gt $mediumFindingsCountFailThreshold `
	-or $scansummary.severitysummary.low -gt $lowFindingsCountFailThreshold)
	{
	   Write-Error "Unhealthy scan result, major vulnerabilities  found in image summary"
	}
	else
	{
		Write-Warning "Unhealthy scan result, some vulnerabilities  found in image"
	}
}
else
{
	Write-Error "Unknown scan result returned"
}

The script is using the Azure Resource Graph to query Azure Security Center to obtain the vulnerability scan results.

Azure Resource Graph is a service in Azure that is designed to extend ARM by providing the ability to query across a given set of subscriptions.
To use it, you must have at least read access to the resources you want to query. Without at least read permissions to the Azure object or object group, results won’t be returned.

If you’re curious about using Azure Resource Graph to query ASC, the following link has a bunch of sample queries: https://docs.microsoft.com/en-us/azure/security-center/resource-graph-samples?tabs=azure-cli

After obtaining the scan results from ARG, the script checks if the status of the scan is unhealthy and if so, it breaks the pipeline execution using the Write-Error command.

3- Sysdig

Sysdig is a SaaS (Software-as-a-Service) platform. It is built on an OSS stack that includes Falco and sysdig OSS.
It provides a stand-alone inline scanner that can perform local analysis on container images and post the result of the analysis to the SaaS backend.

The Sysdig inline scanner works as an independent container, without any Docker dependency, and can analyze images using different image formats and sources.
When the inline scanner has completed the image analysis, it sends the metadata to the Sysdig SaaS to perform a “policy evaluation” step.
A policy is a combination of rules and actions that should be taken if the policy rule is breached.

Here’s a code snippet showing how you can run the Sysdig inline scanner with Azure Pipelines:


trigger:
  branches:
    include:
    - master
  paths:
    exclude:
    - pipelines/*
    - test/*
    - README.md
    - .dockerignore
    - .gitignore

variables:
- name: appName
  value: 'ScanApp'
- name: tag
  value: '$(Build.BuildId)' 


pool:
  vmImage: 'ubuntu-latest'

steps:
- task: Bash@3
  displayName: Create application image
  inputs:
    targetType: 'inline'
    script: |
      docker build -t  ${{ lower(variables.appName) }}:$(tag) .      
    workingDirectory: '$(System.DefaultWorkingDirectory)'
- task: Bash@3
  displayName: Check vulnerabilities
  inputs:
    targetType: 'inline'
    script: |
      docker run --rm \
        -v /var/run/docker.sock:/var/run/docker.sock \
        sysdiglabs/secure-inline-scan:2 \
        --sysdig-url $(SYSDIG_URL) \
        --sysdig-token $(SYSDIG_API_TOKEN) \
        --storage-type docker-daemon \
        --storage-path /var/run/docker.sock \
        ${{ lower(variables.appName) }}:$(tag)      

As you can see, it is a pretty straightforward pipeline, but anyways let’s review the steps.

  • Create the image of your app.
- task: Bash@3
  displayName: Create application image
  inputs:
    targetType: 'inline'
    script: |
      docker build -t  ${{ lower(variables.appName) }}:$(tag) .      
    workingDirectory: '$(System.DefaultWorkingDirectory)'
  • Run the Sysdig secure-inline-scan container image.

When running the scanner there are multiple execution possibilities available:

  • Scan a local image build (storage-type="docker-daemon)
  • Scan a image tarball (storage-type="docker-archive)
  • Scan a OCI tarball (storage-type="oci-archive)
  • Scan a OCI directory (storage-type="oci-dir)

In the previous step we built a docker image, so we’re going to choose to scan a local image build, which means that the storage-type attribute needs to be set to docker-daemon.

Also when scanning a local image build, as in our case, mounting the host Docker socket is required.

The sysdig-token is a token from the Sysdig API that can be retrieved from the SaaS itself, and the sysdig-url is the url of your Sysdig instance.

- task: Bash@3
  displayName: Check vulnerabilities
  inputs:
    targetType: 'inline'
    script: |
      docker run --rm \
        -v /var/run/docker.sock:/var/run/docker.sock \
        sysdiglabs/secure-inline-scan:2 \
        --sysdig-url $(SYSDIG_URL) \
        --sysdig-token $(SYSDIG_API_TOKEN) \
        --storage-type docker-daemon \
        --storage-path /var/run/docker.sock \
        ${{ lower(variables.appName) }}:$(tag)      

When the inline scanner has completed the image analysis, it will send the result to the Sysdig SaaS to perform the policy evaluation step.
If the Policy evaluation fails the pipeline will break, otherwise the pipeline will go on and now you can add an extra step that pushes the image into your trusted repository.

4- AquaSec Trivy

Trivy is an OSS vulnerability scanner for containers and other artifacts, using it with Azure Pipelines it’s really simple and straightforward.

Here’s a code snippet showing how you can run the Trivy scanner with Azure Pipelines:

trigger:
  branches:
    include:
    - master
  paths:
    exclude:
    - pipelines/*
    - test/*
    - README.md
    - .dockerignore
    - .gitignore

variables:
- name: appName
  value: 'ScanApp'
- name: tag
  value: '$(Build.BuildId)' 

pool:
  vmImage: 'ubuntu-latest'

steps:
- task: Bash@3
  displayName: Create application image
  inputs:
    targetType: 'inline'
    script: |
      docker build -t  ${{ lower(variables.appName) }}:$(tag) .      
    workingDirectory: '$(System.DefaultWorkingDirectory)'
- task: Bash@3
  displayName: Download Trivy Scanner
  inputs:
    targetType: 'inline'
    script: |
      curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin      
- task: Bash@3
  displayName: Scan image
  inputs:
    targetType: 'inline'
    script: |
      trivy --exit-code 0 --severity LOW,MEDIUM,HIGH --no-progress ${{ lower(variables.appName) }}:$(tag)
      trivy --exit-code 1 --severity CRITICAL --no-progress ${{ lower(variables.appName) }}:$(tag)          

Let’s review the pipeline steps.

  • Create the image of your app.
- task: Bash@3
  displayName: Create application image
  inputs:
    targetType: 'inline'
    script: |
      docker build -t  ${{ lower(variables.appName) }}:$(tag) .      
    workingDirectory: '$(System.DefaultWorkingDirectory)'
  • Download and install the scanner.
- task: Bash@3
  displayName: Download Trivy Scanner
  inputs:
    targetType: 'inline'
    script: |
      curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin      
  • Run the scanner.

By default, Trivy exits with code 0 even when vulnerabilities are detected. Use the --exit-code option if you want to exit with a non-zero exit code.
In this step the scan will ran twice, but it will break the pipeline with an exit code 1 only when a critical vulnerability is found.

- task: Bash@3
  displayName: Scan image
  inputs:
    targetType: 'inline'
    script: |
      trivy --exit-code 0 --severity LOW,MEDIUM,HIGH --no-progress ${{ lower(variables.appName) }}:$(tag)
      trivy --exit-code 1 --severity CRITICAL --no-progress ${{ lower(variables.appName) }}:$(tag)       

As I stated before if the scanner finds any critical vulnerability the script will exit with an exit code 1 and the pipeline will break, otherwise the pipeline will go on and now you can add another step to push the image into your trusted repository.

Useful links