Scanning container vulnerabilities and publishing the results using Trivy in Azure DevOps

Utkarsh Shigihalli
4 min readFeb 28, 2023

With rapid releases of software, you equally need solid automation and security scanning implemented the moment developers are ready to commit code. Whether it is application code or your container images, you must ensure they are protected against vulnerabilities and scanned regularly. With ever-looming vulnerability threats, scanning your docker images before they are pushed to your container registry is absolutely necessary.

In this post, we will see how you can scan docker images and automate the scanning of vulnerabilities using Trivy — an open-source tool to scan vulnerabilities and also publish the test results to Azure DevOps.

For this demo, I have a small NodeJS app containerised using a multi-stage Dockerfile.

FROM node:16-alpine AS build

WORKDIR /usr/src/app

COPY package.json .
COPY package-lock.json .

RUN npm install

COPY . .

RUN npm prune --production

FROM node:16-alpine
WORKDIR /usr/src/app

COPY --from=build /usr/src/app .
COPY --from=build /usr/src/app/node_modules ./node_modules

CMD ["npm", "start"]

Creating pipeline in Azure DevOps

The first step is to commit the code in source control and link it to Azure DevOps. We are maintaining our Dockerfile and also the YAML file for our pipeline in GitHub.

The next step is to connect Azure DevOps to the GitHub repo. To do that, go to Azure DevOps, and click new Pipeline in the Pipelines service and select GitHub.

You will be able to select the repository and the pipelineyml file

Pipeline YAML to scan

Our Azure DevOps pipeline is simple, I will show you the result first and then YAML.

As you can see in the screenshot below, we would like to see the vulnerabilities in our built Docker images and vulnerabilities in the pipeline results.

This lets us easily monitor HIGH or CRITICAL vulnerabilities and let Azure DevOps bring other insights like is a new/old vulnerability, how long the build failing since etc.

The YAML for our pipeline is only a few lines.

# azure pipeline to build and scan docker image using trivy

trigger:
branches:
include:
- main
paths:
include:
- src/npm-logging

pool:
vmImage: 'ubuntu-latest'

steps:

- script: |
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin v0.37.3
displayName: install trivy

- script: |
cp $(system.defaultworkingdirectory)/src/npm-logging/.azuredevops/junit.tpl $(system.defaultworkingdirectory)/junit.tpl
displayName: 'Copy junit.tpl'

- script: |
docker build -t npm-logging:latest src/npm-logging
displayName: 'Build docker image'

- script: |
docker images
trivy image --severity HIGH,CRITICAL npm-logging:latest --format template --template "@junit.tpl" -o $(System.ArtifactsDirectory)/junit-report-crit-high-app.xml --ignore-unfixed
displayName: 'Run trivy scan'

- task: PublishTestResults@2
displayName: "publish CRITICAL and HIGH vulnerabilities"
inputs:
testResultsFormat: "JUnit"
testResultsFiles: "$(System.ArtifactsDirectory)/junit-report-crit-high-app.xml"
mergeTestResults: true
failTaskOnFailedTests: true
testRunTitle: "npm-logging - critical and high vulnerabilities"

At the beginning of the file, we have a few triggers for our pipeline which allows us to monitor the main branch and trigger our pipeline if any changes.

Next, in the steps node, we first install Trivy using their install script and specify a specific tag (in our case v0.37.3) to download. The tag can be any version of their releases.

The next step is to copy the JUnit XML transformation file where our Trivy can use it. This is needed to transform the scan results into a format which Azure DevOps can understand. Azure DevOps can understand the JUnit test result format and thankfully Trivy provides us with this transformation XML.

After that, the next two steps involve building the docker image and scanning the vulnerabilities using the CLI — so we use trivy image command. We are only interested in HIGH and CRITICAL vulnerabilities so we pass --severity HIGH,CRITICAL option. There are situations where vulnerabilities exist, but the fix is not made available by the vendors. We would like to exclude them using --ignore-unfixed option. Lastly we tell trivy to use our template file to transform the results using --template option.

Last step in the pipeline is to publish the generated results so that Azure DevOps can see them and display in build results. We use PublishTestResults@2 task. If any HIGH or CRITICAL vulnerabilities are found, we would like our build to fail so that team can work to fix the vulnerabilities. We do that by setting failTaskOnFailedTests: true for the task.

Thats, it — save and run the pipeline. You will see because our Dockerfile uses legacy node:16 , it has a vulnerability and hence the build failed. This ensures our docker image with vulnerabilities never gets pushed to ACR and developers have a chance to fix it very early in the software development lifecycle.

Conclusion

Ensuring you embed security scanning very early in the CI/CD process is highly beneficial to teams. This shift-left approach of embedding security in your software development lifecycle allows you to catch vulnerabilities early and rely on automation processes, thus reducing manual checks which are time-consuming. Cross-platform and open-source tools like Trivy make it fairly easy to embed security scanning in any of your CI/CD tools, whether it is Azure DevOps or GitHub Actions.

--

--

Utkarsh Shigihalli

Microsoft MVP | Developer | Passionate about Cloud, .NET and DevOps