# 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](https://www.docker.com/products/docker-desktop/), authenticated
  to `dhi.io`. Sign in with `docker login dhi.io`. Docker Desktop includes
  Docker Scout, which fetches the VEX attestation.
- A vulnerability scanner. This guide shows examples for Docker Scout,
  [Trivy](https://trivy.dev/), and [Grype](https://github.com/anchore/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.

> [!WARNING]
>
> Exposing 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:

```console
$ docker login dhi.io
```

Then pull the image:

```console
$ 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**



```console
$ trivy image --scanners vuln dhi.io/python:3.13
```

If Trivy isn't installed, run it in a container:

```console
$ 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:

```plaintext
Total: 30 (UNKNOWN: 0, LOW: 15, MEDIUM: 11, HIGH: 4, CRITICAL: 0)
```

**Grype**



```console
$ grype dhi.io/python:3.13
```

If Grype isn't installed, run it in a container:

```console
$ docker run --rm \
  -v /var/run/docker.sock:/var/run/docker.sock \
  anchore/grype:latest docker:dhi.io/python:3.13
```

Example output:

```plaintext
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:

```console
$ 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.

> [!NOTE]
>
> Docker 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 `jq` queries in Steps 5 and 6.

## Step 3: Scan with VEX applied

**Docker Scout**



Docker Scout automatically fetches and applies the VEX attestation with no local
file needed:

```console
$ docker scout cves dhi.io/python:3.13
```

Example output:

```plaintext
    ✓ SBOM obtained from attestation, 47 packages indexed
    ✓ Provenance obtained from attestation
    ✓ VEX statements obtained from attestation
    ✓ No vulnerable package detected
```

**Trivy**



Pass the VEX file with the `--vex` flag:

```console
$ trivy image --scanners vuln --vex python-vex.json dhi.io/python:3.13
```

If Trivy isn't installed, run it in a container:

```console
$ 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:

```plaintext
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.
```

**Grype**



Pass the VEX file with the `--vex` flag:

```console
$ grype dhi.io/python:3.13 --vex python-vex.json
```

If Grype isn't installed, run it in a container:

```console
$ 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:

```plaintext
No vulnerabilities found
```



Same 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.

```console
$ 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:

```console
$ 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:

```plaintext
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](/dhi/core-concepts/vex/#not_affected-justification-codes).

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:

```console
$ jq '.statements[] | select(.vulnerability.name == "CVE-2010-4756") | {status, justification, status_notes}' python-vex.json
```

Example output:

```json
{
  "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.

> [!IMPORTANT]
>
> PURL matching is strict. Scanners must match VEX statements to packages
> using the full PURL string, including the `os_name`, `os_version`, and
> `os_distro` qualifiers. 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:

```console
$ jq '[.statements[].status] | group_by(.) | map({status: .[0], count: length})' python-vex.json
```

List all CVEs under active investigation:

```console
$ jq '[.statements[] | select(.status == "under_investigation") | {cve: .vulnerability.name, products: [.products[]."@id"]}]' python-vex.json
```

List any CVEs with `affected` status:

```console
$ 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:

```console
$ 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](/dhi/core-concepts/vex/#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](/dhi/how-to/scan/).
- 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](/scout/how-tos/create-exceptions-vex/).
- VEX status reference: For status definitions, justification codes, and
  why DHI does not use `fixed`, see [Vulnerability Exploitability eXchange
  (VEX)](/dhi/core-concepts/vex/#vex-status-reference).
- Browse the VEX feed directly: The raw VEX data is published at
  [github.com/docker-hardened-images/advisories](https://github.com/docker-hardened-images/advisories/tree/main/vex),
  organized by image name.

