Chapter 2
10 min read

Exploring Container Security Tools

Docker Scout

Docker Scout is a container security tool, developed by Docker Inc. and released as "GA" (generally available) in October 2023 at DockerCon. I started writin...

Docker Scout is a container security tool, developed by Docker Inc. and released as "GA" (generally available) in October 2023 at DockerCon. I started writing this chapter 5 days after the release, so Docker Scout would most probably change rapidly in the coming months.

Installation

Docker Scout is bundled into Docker Desktop, so there is no need to install anything extra to use it, if you're using Docker Desktop.

To install the standalone CLI plugin, confer the official installation instructions.

Analyzing Container Images

To list the CVEs in a Docker image, say alpine:3.12, run the following command:

$ docker scout cves alpine:3.12

The output of the command will be similar to the following:

    ✓ SBOM of image already cached, 18 packages indexed
    ✗ Detected 1 vulnerable package with 1 vulnerability

### Packages and Vulnerabilities

   1C     0H     0M     0L  zlib 1.2.12-r0
pkg:apk/alpine/zlib@1.2.12-r0?os_name=alpine&os_version=3.12

    ✗ CRITICAL CVE-2022-37434
      https://scout.docker.com/v/CVE-2022-37434
      Affected range : <1.2.12-r2  
      Fixed version  : 1.2.12-r2   
    
1 vulnerability found in 1 package
  LOW       0  
  MEDIUM    0  
  HIGH      0  
  CRITICAL  1  

What's Next?
  Learn more about base image update recommendations → 
    docker scout recommendations alpine:3.12

The first line of the report says that SBOM for the image is already cached, and 18 packages are indexed. Because of this, Scout doesn't need to scan the image for vulnerabilities, and can directly cross-reference the packages with the vulnerabilities database. A few lines later, it says that there is one critical vulnerability in the zlib, as we saw with Trivy and Snyk.

Docker and SBOM

Docker added support for SBOM generation after the Log4Shell incident in 2021. To generate SBOM for a Docker image, run the following command:

$ docker sbom alpine:3.12

This command works on Docker Desktop, but needs Docker SBOM CLI plugin otherwise. The output of the command will be similar to the following:

Syft v0.43.0
 ✔ Loaded image            
 ✔ Parsed image            
 ✔ Cataloged packages      [14 packages]

NAME                    VERSION      TYPE 
alpine-baselayout       3.2.0-r7     apk   
alpine-keys             2.4-r0       apk   
apk-tools               2.10.8-r1    apk   
busybox                 1.31.1-r22   apk   
ca-certificates-bundle  20211220-r0  apk   
libc-utils              0.7.2-r3     apk   
libcrypto1.1            1.1.1n-r0    apk   
libssl1.1               1.1.1n-r0    apk   
libtls-standalone       2.9.1-r1     apk   
musl                    1.1.24-r10   apk   
musl-utils              1.1.24-r10   apk   
scanelf                 1.2.6-r0     apk   
ssl_client              1.31.1-r22   apk   
zlib                    1.2.12-r0    apk 

The output is a list of packages installed in the image. The first line shows that the image was loaded and then parsed with Syft. We introduced Syft in the previous chapter.

The Alpine image here has the SBOM already cached, so Scout didn't need to generate it. One can also generate SBOM upon image build, using the --sbom option. Let's build an Alpine image with curl installed, and generate SBOM for it:

FROM alpine:3.12

RUN apk add curl

Now build the image with the --sbom option:

$ docker buildx build --sbom=true -t alpine-curl .

The output is similar to the following:

[+] Building 3.4s (10/10) FINISHED                                                                                                                     docker:desktop-linux
 => [internal] load build definition from Dockerfile                                                                                                                   0.0s
 => => transferring dockerfile: 71B                                                                                                                                    0.0s
 => [internal] load .dockerignore                                                                                                                                      0.0s
 => => transferring context: 2B                                                                                                                                        0.0s
 => resolve image config for docker.io/docker/buildkit-syft-scanner:stable-1                                                                                           2.0s
 => [auth] docker/buildkit-syft-scanner:pull token for registry-1.docker.io                                                                                            0.0s
 => [internal] load metadata for docker.io/library/alpine:3.12

The output shows that the image was built with the docker/buildkit-syft-scanner image, which is used to generate SBOM. The command used for building the image is docker buildx build, which is different from the usual docker build command in the available options. The buildx was added to Docker CLI to support extended build options, for example, building multi-architecture images. It uses BuildKit as the build engine.

BuildKit is different from the usual Docker build engine in that it doesn't rely on the Docker daemon. It's important as the Docker daemon is a privileged process, and can be used to escalate privileges. BuildKit is also faster than the usual Docker build engine, and can be used to build multi-architecture images.

Using the --sbom option, the SBOM is generated and bundled into the image, but only if we push to the registry also on the same go:

$ docker buildx build --sbom=true -t aerabi/alpine-curl --push .

The SBOM is then uploaded to the registry along with the image. The SBOM can also be generated locally, using the --output option, to check it before pushing the image to the registry:

$ docker buildx build --sbom=true -o type=local,dest=out -t alpine-curl .

On the root of out you can find the SBOM attestation file named sbom.spdx.json. The file is in SPDX format.

To explore SBOMs bundled into a Docker image, you can use the imagetools inspect command:

$ docker buildx imagetools inspect aerabi/alpine-curl

The output of the command will be similar to the following:

Name:      docker.io/aerabi/alpine-curl:latest
MediaType: application/vnd.docker.distribution.manifest.v2+json
Digest:    sha256:b210d83491f5dcc49702bb7b874af3c25a9c2db444ebdeab4d7c3f7ab2e66cd5

To generate the SBOM with a different tool, you can use the generator option under the --attest option:

$ docker buildx build \
  --attest type=sbom,generator=<generator_image> \
  -o type=local,dest=out .

Docker Scout created an SBOM indexer, which can be used with the --attest option:

$ docker buildx build \
  --attest type=sbom,generator=docker/scout-sbom-indexer:d3f9c2d \
  -o type=local,dest=out .

If a Docker image has an SBOM attestation, Docker Scout will pick it up and use it for its analysis.

Docker Scout Output Formats

Docker Scout supports the following output formats:

  • Packages: A list in plain text
  • SARIF: The one that we want to use in CI/CD pipelines
  • Markdown: Which also includes HTML tags
  • SPDX: SPDX JSON format
  • GitLab: GitLab Security Report format
  • SBOM: SBOM in SPDX JSON format

The following is an example of the SARIF output:

$ docker scout cves --format sarif --output alpine.sarif.json alpine:3.12

As before, CI/CD pipelines can use the SARIF output to upload the scan results to the GitHub Security tab.

GitHub Actions

Docker Scout can be used as a GitHub Action for different purposes. One would be comparing the newly built image with the one in the registry in terms of vulnerabilities. The following is an example of a GitHub Actions workflow that uses Docker Scout Action:

      - name: Run Docker Scout to extract vulnerabilities from the image
        uses: docker/scout-action@v1
        with:
          command: cves
          image: '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}'
          sarif-file: scout.sarif
          github-token: '${{ github.token }}'

      - name: Upload Docker Scout scan results to GitHub Security tab
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: scout.sarif

In this example, we used the docker/scout-action to extract the CVEs from the image. The output is stored in the file scout.sarif, and then uploaded to the GitHub Security tab using the github/codeql-action.

We could use another command, e.g. compare to compare the vulnerabilities of the new image with the one in the registry:

      - name: Run Docker Scout to compare vulnerabilities with the latest image
        uses: docker/scout-action@v1
        with:
          command: compare
          image: '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}'
          to-latest: true
          github-token: '${{ github.token }}'

And we can use multiple commands at the same time:

          command: compare,cves,quickview

The github-token is used to write the result as a comment on the pull request. The to-latest option is used to compare the image with the latest one in the registry.

Docker Scout and Vulnerability Exploitability

Vulnerability scanning for Docker images was introduced by Docker in 2020, with the docker scan subcommand. Docker Scan was using Snyk under the hood.

Docker Scout was initially released as a replacement for the docker scan subcommand, in March 2023. I wrote an article about it on Medium then, comparing their outputs. There, I mentioned that the two commands have found different CVEs in the same image. A day later, Docker contacted me and mentioned that they have investigated the differences, and that most of them are false positives. In one case, the installed version was higher than the fixed version, and in another case, package names were different (the package github.com/labstack/echo was being confused with github.com/labstack/echo/v4, which is a different package). They also mentioned that they are working on their false positive CVE with the source (that was an advisory on GitLab).

The main point here is that it's always good to check the CVEs manually. The CVEs reported could be false positive, or could be irrelevant to your project. Only because a CVE is in your image, it doesn't mean that it's exploitable. The exploitability of a CVE depends on the context of the image, and the application running in it. This is why you can automatically ignore some CVEs when using Docker Scout.

Vulnerability Exploitability eXchange (VEX) is a standard format to document the exploitability of a vulnerability in the context of a specific image. You can use this format to create exceptions for vulnerabilities that are not exploitable in your image.

To create a VEX document, we need a tool called vexctl. You can install it with Homebrew or using the instruction on their GitHub repo.

After installation, use the following command to create a VEX file:

$ vexctl create \
  --author="mohammad-ali@aerabi.com" \
  --product="pkg:docker/alpine@3.12" \
  --vuln="CVE-2022-37434" \
  --status="not_affected" \
  --justification="vulnerable_code_not_in_execute_path" \
  --file="CVE-2022-37434.vex.json"

A JSON file is created with the following content:

{
  "@context": "https://openvex.dev/ns/v0.2.0",
  "@id": "https://openvex.dev/docs/public/vex-64dc6e2613b24e0638c10220c4dca",
  "author": "mohammad-ali@aerabi.com",
  "timestamp": "2025-03-17T14:16:27.900702+01:00",
  "version": 1,
  "statements": [
    {
      "vulnerability": {
        "name": "CVE-2022-37434"
      },
      "timestamp": "2025-03-17T14:16:27.900703+01:00",
      "products": [
        {
          "@id": "pkg:docker/alpine@3.12"
        }
      ],
      "status": "not_affected",
      "justification": "vulnerable_code_not_in_execute_path"
    }
  ]
}

To use this VEX file with Docker Scout, you can use the --vex-location option:

$ docker scout cves alpine:3.12 --vex-location .

The --vex-location flag is used to specify the location of the VEX files. Here we used the current directory, but you can change it to the directory where the VEX files are stored. The result will be similar to the following:

## Packages and Vulnerabilities

   1C     0H     0M     0L  zlib 1.2.12-r0
pkg:apk/alpine/zlib@1.2.12-r0?os_name=alpine&os_version=3.12

    ✗ CRITICAL CVE-2022-37434
      https://scout.docker.com/v/CVE-2022-37434
      Affected range : <1.2.12-r2                                          
      Fixed version  : 1.2.12-r2                                           
      VEX            : not affected [vulnerable code not in execute path]  
                     : mohammad-ali@aerabi.com                             

To suppress the "not affected" CVEs from the output, you can use the --only-vex-affected option:

$ docker scout cves alpine:3.12 --vex-location . --only-vex-affected

The output will ignore the CVE altogether and report only the affected ones (which are none at the time of writing, because the suppressed CVE was the only one in the image).

Exercises

  1. Build an image and generate SBOM for it using BuildKit. Then push the image to Docker Hub.

  2. Check the image for known vulnerabilities using Docker Scout:

    $ docker scout cves aerabi/alpine-curl
    
  3. Generate SBOM for a Docker image using Docker Scout:

    $ docker scout sbom aerabi/alpine-curl
    
  4. Get recommendations for a Docker image using Docker Scout:

    $ docker scout recommendations aerabi/alpine-curl
    
  5. Create a VEX file for a CVE in your image, and push it to the registry as an attestation for your image:

    $ docker scout attestation add \
      --file <cve-id>.vex.json \
      --predicate-type https://openvex.dev/ns/v0.2.0 \
      IMAGE
    
  6. Create a GitHub Actions workflow that uses Docker Scout to scan a Docker image for known vulnerabilities, and uploads the results to the GitHub Security tab. Add it to your project of choice.

  7. Create a GitHub Actions workflow that uses Docker Scout to compare a newly built Docker image with the one in the registry in terms of vulnerabilities, and uploads the results to the GitHub Security tab. Add it to your project of choice.