Explore VEX statements in Docker Hardened Images
Standard vulnerability scanners report CVEs against packages present in an image. With Docker Hardened Images, those packages are there by design, but each reported CVE has a VEX statement explaining whether it is exploitable in this specific product configuration. This guide walks through scanning a Docker Hardened Image with and without VEX and auditing the justification behind every suppression.
Prerequisites
- Docker Desktop, authenticated
to
dhi.io. Sign in withdocker login dhi.io. Docker Desktop includes Docker Scout, which fetches the VEX attestation. - A vulnerability scanner. This guide shows examples for Docker Scout, Trivy, and Grype. Trivy and Grype can also run as Docker containers with no installation needed.
jq(optional), for filtering the VEX file.
Expose daemon for Windows users
The -v /var/run/docker.sock:/var/run/docker.sock socket mount used in the
containerized scanner commands throughout this guide does not work on Docker
Desktop for Windows. To use containerized scanners on Windows, go to
Settings > General in Docker Desktop and turn on Expose daemon on
tcp://localhost:2375 without TLS. Then replace
-v /var/run/docker.sock:/var/run/docker.sock with
-e DOCKER_HOST=tcp://host.docker.internal:2375 in every containerized
scanner command.
WarningExposing the daemon on TCP without TLS makes your system vulnerable to remote code execution attacks. Turn off the setting when you are done testing.
Step 1: Scan without VEX
Sign in to the Docker Hardened Images registry:
$ docker login dhi.io
Then pull the image:
$ docker pull dhi.io/python:3.13
Then scan without VEX to see the raw CVE count. Docker Scout automatically applies VEX on Docker Hardened Images. To see the unfiltered CVE baseline, use Trivy or Grype.
$ trivy image --scanners vuln dhi.io/python:3.13
If Trivy isn't installed, run it in a container:
$ docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy:latest image --scanners vuln dhi.io/python:3.13
Example output:
Total: 30 (UNKNOWN: 0, LOW: 15, MEDIUM: 11, HIGH: 4, CRITICAL: 0)$ grype dhi.io/python:3.13
If Grype isn't installed, run it in a container:
$ docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
anchore/grype:latest docker:dhi.io/python:3.13
Example output:
NAME INSTALLED FIXED IN TYPE VULNERABILITY SEVERITY
libc6 2.41-12+deb13u2 deb CVE-2018-20796 Negligible
libc6 2.41-12+deb13u2 (won't fix) deb CVE-2026-4437 High
libc6 2.41-12+deb13u2 (won't fix) deb CVE-2026-5450 Critical
...The output lists CVEs across libc6, libncursesw6, libsqlite3-0, libuuid1,
zlib1g, and others, all runtime dependencies that Python needs to function.
These packages are present by design.
A scan result like this doesn't mean every reported CVE requires patching. It means these CVEs have been reported against packages present in the image. Whether any of those CVEs are actually exploitable in this configuration is a separate question, and that's exactly what VEX answers.
Step 2: Fetch the VEX attestation
Export the VEX attestation to a local file:
$ docker scout vex get registry://dhi.io/python:3.13 --output python-vex.json
The registry:// prefix tells Scout to fetch the attestation from the registry
rather than the local image store. Because you pulled the image in Step 1, it
already exists locally, and without this prefix Scout would find no attestation
there.
This fetches a signed OpenVEX document from registry.scout.docker.com,
Docker's supply chain metadata registry for all Docker Hardened Images. The
document records Docker's exploitability assessment for every CVE found in the
image's SBOM.
NoteDocker Scout fetches this file automatically when scanning. You only need to download it explicitly for scanners that don't natively integrate it, or to run the
jqqueries in Steps 5 and 6.
Step 3: Scan with VEX applied
Docker Scout automatically fetches and applies the VEX attestation with no local file needed:
$ docker scout cves dhi.io/python:3.13
Example output:
✓ SBOM obtained from attestation, 47 packages indexed
✓ Provenance obtained from attestation
✓ VEX statements obtained from attestation
✓ No vulnerable package detectedPass the VEX file with the --vex flag:
$ trivy image --scanners vuln --vex python-vex.json dhi.io/python:3.13
If Trivy isn't installed, run it in a container:
$ docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "$(pwd)/python-vex.json:/tmp/vex.json" \
aquasec/trivy:latest image --scanners vuln --vex /tmp/vex.json dhi.io/python:3.13
Example output:
Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)
Some vulnerabilities have been ignored/suppressed. Use the '--show-suppressed' flag to display them.Pass the VEX file with the --vex flag:
$ grype dhi.io/python:3.13 --vex python-vex.json
If Grype isn't installed, run it in a container:
$ docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "$(pwd)/python-vex.json:/tmp/vex.json" \
anchore/grype:latest docker:dhi.io/python:3.13 --vex /tmp/vex.json
Example output:
No vulnerabilities foundSame image, same packages, same CVE database. The only difference is context. The scanner matched each CVE against the VEX file and suppressed every one that Docker assessed as not exploitable.
The packages are still there. Check the SBOM and you will see libc6,
libsqlite3-0, and every other package from Step 1. Zero CVEs does not mean
the packages were removed. It means each reported CVE has a documented reason
why it does not apply to this product configuration.
VEX is an open standard: the attestation travels with the image and any compliant scanner reads the same reasoning.
Step 4: Inspect every suppression and its justification
Docker Scout and Grype suppress VEX-matched CVEs but do not surface the
justification code in their output. Use Trivy's --show-suppressed flag to see
every suppressed CVE alongside its per-CVE justification code.
$ trivy image --scanners vuln --vex python-vex.json --show-suppressed dhi.io/python:3.13
If Trivy isn't installed, run it in a container:
$ docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "$(pwd)/python-vex.json:/tmp/vex.json" \
aquasec/trivy:latest image --scanners vuln --vex /tmp/vex.json --show-suppressed dhi.io/python:3.13
Example output:
Suppressed Vulnerabilities (Total: 28)
======================================
┌──────────────┬──────────────────┬──────────┬──────────────┬───────────────────────────────────────────────────┐
│ Library │ Vulnerability │ Severity │ Status │ Statement │
├──────────────┼──────────────────┼──────────┼──────────────┼───────────────────────────────────────────────────┤
│ libc6 │ CVE-2010-4756 │ LOW │ not_affected │ vulnerable_code_cannot_be_controlled_by_adversary │
│ libsqlite3-0 │ CVE-2025-70873 │ LOW │ not_affected │ vulnerable_code_not_present │
│ ... │ ... │ ... │ ... │ ... │
└──────────────┴──────────────────┴──────────┴──────────────┴───────────────────────────────────────────────────┘The Statement column shows the machine-readable justification code from the
VEX file.
The justification codes have precise meanings:
vulnerable_code_cannot_be_controlled_by_adversary: the vulnerable code path exists in the package, but an attacker cannot trigger it in this configuration.vulnerable_code_not_present: the vulnerable code was not compiled into this build or is otherwise absent.inline_mitigations_already_exist: Docker has applied a backport or patch that addresses the CVE in this image.
For the full list of justification codes, see VEX status reference.
Every suppression is documented, auditable, and verifiable with any VEX-enabled scanner.
Step 5: Read Docker's reasoning for a specific CVE
The justification codes are machine-readable; the status_notes field in the
VEX file contains Docker's human-readable reasoning. Use jq to look up a
specific CVE:
$ jq '.statements[] | select(.vulnerability.name == "CVE-2010-4756") | {status, justification, status_notes}' python-vex.json
Example output:
{
"status": "not_affected",
"justification": "vulnerable_code_cannot_be_controlled_by_adversary",
"status_notes": "Standard POSIX behavior in glibc. Applications using glob need to impose limits themselves. Requires authenticated access and is considered unimportant by Debian."
}The status_notes field explains Docker's reasoning in plain language. For
CVE-2010-4756, the glob behavior described by the CVE is standard POSIX
behavior, requires authenticated access, and is classified as unimportant by
the Debian security team.
Each statement also lists the affected products as Package URLs (PURLs), for
example pkg:deb/debian/glibc@2.41-12%2Bdeb13u2?os_distro=trixie&os_name=debian&os_version=13.
Trivy matched this statement to libc6 in the image's SBOM by comparing that
PURL against the packages recorded in the SBOM.
ImportantPURL matching is strict. Scanners must match VEX statements to packages using the full PURL string, including the
os_name,os_version, andos_distroqualifiers. Matching on package name alone risks applying a suppression from one OS version to a different version where the CVE is exploitable.
Step 6: Filter VEX statements by status
Once you have python-vex.json, use jq to query it directly.
Count statements by status:
$ jq '[.statements[].status] | group_by(.) | map({status: .[0], count: length})' python-vex.json
List all CVEs under active investigation:
$ jq '[.statements[] | select(.status == "under_investigation") | {cve: .vulnerability.name, products: [.products[]."@id"]}]' python-vex.json
List any CVEs with affected status:
$ jq '[.statements[] | select(.status == "affected") | {cve: .vulnerability.name, action: .action_statement}]' python-vex.json
The affected query returns an empty array for the current dhi.io/python:3.13
image, which is the expected result for an actively maintained tag. To see affected
entries across all DHI Python versions, query the full VEX feed:
$ curl -s https://raw.githubusercontent.com/docker-hardened-images/advisories/main/vex/python/dhi-python.vex.json \
| jq '[.statements[] | select(.status == "affected") | {cve: .vulnerability.name, action: .action_statement}]'
For status definitions and justification codes, see the VEX status reference.
What's next
- Scan with other tools: Learn how to apply DHI VEX statements with Trivy (VEX Hub) and Grype in Scan Docker Hardened Images.
- Write your own VEX for child images: If you build on top of a DHI and want to suppress CVEs in packages you add, see Create an exception using VEX.
- VEX status reference: For status definitions, justification codes, and
why DHI does not use
fixed, see Vulnerability Exploitability eXchange (VEX). - Browse the VEX feed directly: The raw VEX data is published at github.com/docker-hardened-images/advisories, organized by image name.