Introduction
In my previous article, I discussed the importance of vulnerability scanning and how Docker Scout can provide a better overview of dependencies and their associated vulnerabilities.
In this article, we will get our hands on Docker Scout by creating a GitHub Workflow to build a docker image and scan it for vulnerabilities before merging its content with a production branch and publishing it to Docker Hub. This process increases the quality of the released application.
Creating a Sample Application
For this tutorial, we will use my Hypnos application code as an example. This is a web application built using React.js and the Yarn package manager. First, we must create its Docker file.
FROM node:14-alpine
WORKDIR /app
COPY package.json ./
COPY yarn.lock ./
RUN yarn install --frozen-lockfile
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
Next, it’s good practice to instruct Docker to prevent the node_modules from being unintentionally copied into the Docker image. This folder might become quite large, and since we will build the image anyway, we don’t need to copy them. To do so, create a .dockerignore file with the following content:
node_modules
Before moving to the actual GitHub Action, let’s build the image locally to ensure everything works as expected.
docker image build -t hypnos:1.0 .
Finally, we can run a container with the newly created image.
docker run -p 3000:3000 hypnos:1.0
Automating the Build and Scanning Using GitHub Actions
Now that we have our working Docker image, we can focus on automating the vulnerability scanning using Docker Scout. Docker has recently released a GitHub action specific to Docker Scout. However, at the time of this writing, this action doesn’t support the creation of output files (which is supported by Docker Scout CLI). For this reason, we won’t use this GitHub Action in this demonstration. Instead, we will use the Docker Scout CLI.
Creating the Secrets
Docker Scout uses a proprietary vulnerability database that operates on a subscription-based model. Therefore, Docker Scout needs the user to authenticate to Docker Hub before scanning the image. To do this, we will store this information in the GitHub secrets.
- Go to Docker Hub and log in with your account.
- Click on your username and select Account Settings from the dropdown.
- Select the Security tab and click New Access Token.
- Give your token a description and choose its access permissions. In this case, we will need Read & Write.
- Click the Generate button and store the token, as it will not be visible again.
Next, create a new GitHub secret and save the Docker Hub token. Then, create another GitHub secret and store the username. In this case, I named them DOCKERHUB_TOKEN and DOCKERHUB_USER.
Creating the GitHub Workflow
First, we need to specify when the workflow will be triggered by defining the triggers. In this case, we will trigger the workflow at any push or pull requests targeting the main branch. Then in the pipeline, we will use conditions to execute a block according to the type of event.
push:
branches:
- main
pull_request:
types: [opened, synchronize, reopened, closed]
branches:
- main
Next, we create an environment variable containing the name of the image.
env:
DOCKER_IMAGE_NAME: hypnos:$(date +%s)
Then, we will install the Docker Scout plugin and build the image.
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Docker Scout
if: ${{ github.event_name == 'pull_request' }}
run: |
curl -fsSL https://raw.githubusercontent.com/docker/scout-cli/main/install.sh -o install-scout.sh
sh install-scout.sh
- name: Build the Docker image
run: docker build . --file Dockerfile --tag $DOCKER_IMAGE_NAME
Next, we can add the Docker Scout action and trigger the command ‘cves’, which will display CVEs identified in the image we previously built (excluding the base image) and output the findings in a JSON file.
- name: Run Docker Scout
if: ${{ github.event_name == 'pull_request' }}
run: |
docker scout cves --ignore-base --format sarif --output hypnos.sarif.json
Based on the number of vulnerabilities found during the scan, we will set an output variable to either true or false.
- name: Check vulnerabilities
id: check_vulnerabilities
if: ${{ github.event_name == 'pull_request' }}
run: |
if [[ $(cat hypnos.sarif.json | jq '.runs[0].results | length') -gt 0 ]]; then
echo -e "\e[31mThere were vulnerabilities in your Docker image. Check the comments on your PR to know more.\e[0m"
echo "fail_workflow=true" >> "$GITHUB_OUTPUT"
else
echo "There were no vulnerabilities in your Docker image. Good job!"
echo "fail_workflow=false" >> "$GITHUB_OUTPUT"
fi
If the number of vulnerabilities exceeds zero, we will create a comment in the pull request, which includes the scan’s rules, results, and overall outcome, and then make the pipeline fail.
- name: Create Comment
if: ${{ github.event_name == 'pull_request' && steps.check_vulnerabilities.outputs.fail_workflow == 'true' }}
uses: actions/github-script@v4
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
const body = `
**JSON File Content:**
\`\`\`
${fs.readFileSync('hypnos.sarif.json', 'utf8')}
\`\`\`
`;
await github.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: body
});
- name: Fail Workflow
if: ${{ github.event_name == 'pull_request' && steps.check_vulnerabilities.outputs.fail_workflow == 'true' }}
run: exit 1
If no vulnerabilities are found, the workflow will complete successfully, and once the pull request is approved and merged to the main branch, we will tag the image appropriately and push it to Docker Hub.
- name: Tag the Docker image
if: ${{ github.event_name == 'push' }}
run: docker image tag $DOCKER_IMAGE_NAME ${{ secrets.DOCKERHUB_USERNAME }}/$DOCKER_IMAGE_NAME
- name: Publish the Docker image
if: ${{ github.event_name == 'push' }}
run: docker image push ${{ secrets.DOCKERHUB_USERNAME }}/$DOCKER_IMAGE_NAME
By adopting this approach and integrating Docker Scout into your CI workflow, you can proactively identify vulnerabilities early in the development process. This allows for prompt remediation and ensures that the applications you release maintain a high level of security and quality.
References