Integrating Docker Scout with GitHub Workflow

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.
    Integrating Docker Scout
  • Give your token a description and choose its access permissions. In this case, we will need Read & Write.
    Integrating Docker Scout
  • 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.

Integrating Docker Scout

References


Similar Articles