Scanning container vulnerabilities and publishing the results using Trivy in Azure DevOps
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
COPY package.json .
COPY package-lock.json .
RUN npm install
COPY . .
RUN npm prune --production
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 pipeline
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
CRITICALvulnerabilities 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
- 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: |
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"
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
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
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
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.
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.