Setting up the Docker image scan GitHub Action
Tags: containersecurity, docker
Published: 2023-06-21 15:45:51

Nowadays, the final product of most Git repositories is a Docker image, that is then used in a Kubernetes deployment. With security being a hot topic now (and for good reasons), it would be  scanning the Docker images you create in the CI is vital.


In this piece, I’ll use GitHub Actions to build Docker images and then scan them for security vulnerabilities. The Docker image built in the CI is also pushed to GitHub’s Docker registry.


![](https://res.cloudinary.com/snyk/image/upload/v1684509342/blog-docker-scan-github-action.jpg)
Creating a GitHub Actions workflow
----------------------------------


The CI workflow we’re going to create has the following structure:


* Test
* Build image
* Scan image


The image built in the second step is then pushed to the GitHub Docker registry and again pulled from it in the third stage.


### Create the test job


Assuming your GitHub repo has no CI configuration, we’re going to create a file in the path `.github/workflows/ci.yaml` with the following content:



```
    name: ci
    on:
      push:
        branches:
          - master
      pull_request:

    jobs:
      test:
        runs-on: ubuntu-latest

        strategy:
          matrix:
            node-version: [ 18.x ]

        steps:
          - name: Checkout 🛎️
            uses: actions/checkout@v3

          - name: Setup Node environment 🧱: Node.js ${{ matrix.node-version }}
            uses: actions/setup-node@v3
            with:
              node-version: ${{ matrix.node-version }}

          - name: Install and test 🪲
            run: |
              npm ci
              npm test
```

The `on` attribute specifies the events that trigger this workflow to run. In this case, any push to the branch `master` and every pull request triggers the CI workflow.


The job steps include the following:


* Checkout 🔔: Gets the code from the repo
* Setup Node environment 🧱: Installs Node.js 18 environment
* Install and test 🪲: Install dependencies and runs the tests


The test job here is merely an example of running the unit tests of a Node.js project. Feel free to alter it to match your project language and structure.


Although it’s not necessary to have a test job to build the Docker image, it’s a good practice, so I stuck to it.


### Create the Docker build job


To create the Docker build job, I would first create two environment variables at the beginning of the file under `on`:



```
  on:
      ...

    env:
      DOCKER_IMAGE_TAG: ${{ github.ref == 'refs/heads/master' && 'prod-' || 'dev-' }}${{ github.sha }}
      GITHUB_REGISTRY: ghcr.io
      GITHUB_REPOSITORY: ${{ github.repository }}
```

Then, let’s create the `build\_docker job`:



```
    jobs:
      test:
        ...

      build_image:
        permissions:
          id-token: write
          contents: read
          packages: write
        runs-on: ubuntu-latest
        needs: [ test ]

        steps:
          - name: Checkout 🛎️
            uses: actions/checkout@v2

          - name: Log in to the Container registry 📦
            uses: docker/login-action@v2
            with:
              registry: ${{ env.GITHUB_REGISTRY }}
              username: ${{ github.actor }}
              password: ${{ secrets.GITHUB_TOKEN }}

          - name: Build and push Docker image 🐳
            uses: docker/build-push-action@v3
            with:
              push: true
              tags: |
                ${{ env.GITHUB_REGISTRY }}/${{ env.GITHUB_REPOSITORY }}:${{ env.DOCKER_IMAGE_TAG }}

```

Let’s look into the code. The permissions section is needed as we want to push Docker images into GitHub’s registry.


The job runs on the latest Ubuntu version and requires the test job to finish. That’s to ensure we’re only creating Docker images for functioning code. If you skipped the test job, remove the needs line.


Now to the steps:


* Checkout 🔔: Gets the code from the repo. This is required to get the Dockerfile and its context.
* Log in to the Container registry 📦
* Build and push Docker image 🐳: In this step, we use the environment variables we introduced at the beginning of the file to specify the Docker image’s tag.


### Create the scan job


Now, let’s add the Docker image scan job:



```
    jobs:
      test:
        ...

      build_image:
        ...

      scan_docker_image:
        permissions:
          id-token: read
          contents: read
          packages: read
        runs-on: ubuntu-latest
        needs: [ build_image ]
        steps:
          - name: Checkout 🛎️
            uses: actions/checkout@v2

          - name: Log in to the Container registry 📦
            uses: docker/login-action@v2
            with:
              registry: ${{ env.GITHUB_REGISTRY }}
              username: ${{ github.actor }}
              password: ${{ secrets.GITHUB_TOKEN }}

          - name: Scan Docker image 🐳
            uses: snyk/actions/docker@master
            continue-on-error: true
            with:
              image: ${{ env.GITHUB_REGISTRY }}/${{ env.GITHUB_REPOSITORY }}:${{ env.DOCKER_IMAGE_TAG }}
              args: --file=Dockerfile --severity-threshold=high --sarif-file-output=snyk.sarif
            env:
              SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}

          - name: Upload Snyk report as sarif 📦
            uses: github/codeql-action/upload-sarif@v2
            with:
              sarif_file: snyk.sarif
```

This step uses Snyk, the security scanning engine behind `docker scan`. To use it, you need to create a free account and store its token in a secret:


* Sign up [here](/)
* Get the token as described [here](https://docs.snyk.io/integrations/snyk-broker/snyk-broker-code-agent/setting-up-the-code-agent-broker-client-deployment/step-1-obtaining-the-required-tokens-for-the-setup-procedure/obtaining-your-snyk-api-token)
* Add it to your GitHub repo’s CI secrets with the name `SNYK\_TOKEN` as explained in the appendix


Now on with the steps:


* Checkout 🛎️: The scanner performs better if it has access to the Dockerfile as well.
* Log in to the Container registry 📦: To get the Docker images we pushed there earlier.
* Scan Docker image 🐳: This job scans the Docker image and reports the vulnerabilities in a file called `snyk.sarif`. This file format is recognized by GitHub and can be shown in the PR — which is why we have the next step
* Upload Snyk report as sarif 📦: Here we upload the `sarif` file we generated in the previous step and upload it to GitHub


The vulnerabilities uploaded to GitHub show up on your PR like this:


![](https://res.cloudinary.com/snyk/image/upload/v1684509342/blog-docker-scan-github-action-unchanged-files.jpg)
Conclusion
----------






In this piece, we created a GitHub Actions workflow with 3 jobs that would run the tests, build the Docker image, push it to the GitHub registry, check it for security issues, and upload the vulnerability report so that GitHub would understand and display them in the PRs.


Appendix: Add CI Secret on GitHub
---------------------------------


In your repo:


1. Click on the settings tab.
2. In the menu panel on the left, from the Security section, click on **Secrets and variables**.
3. Then, from the newly appeared menu items, click on **Actions**.
4. On the top right, click on the green button that says **New repository secret**.
5. In the **Name** section, write down the desired name, e.g. `SNYK\_TOKEN`.
6. In the **Secret** section, paste the secret, e.g. the Snyk token you copied from Snyk website.
7. Then, click the green button that says **Add secret**.


![](https://res.cloudinary.com/snyk/image/upload/v1684509340/blog-docker-scan-github-action-setting.jpg)