Skip to content

Commit 07ce908

Browse files
dhi: add api (#25836)
<!--Delete sections as needed --> ## Description Added DHI API ref to tools and a walkthrough as a how-to topic. - https://deploy-preview-25836--docsdocker.netlify.app/dhi/tools/api/ - https://deploy-preview-25836--docsdocker.netlify.app/dhi/how-to/vex-api/ ## Related issues or tickets ENGDOCS-3364 ## Reviews <!-- Notes for reviewers here --> <!-- List applicable reviews (optionally @tag reviewers) --> - [ ] Technical review - [ ] Editorial review - [ ] Product review --------- Signed-off-by: Craig Osterhout <craig.osterhout@docker.com>
1 parent 654b4fe commit 07ce908

6 files changed

Lines changed: 364 additions & 1 deletion

File tree

content/manuals/dhi/explore/security-concepts/vex.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ status values. DHI uses three of them:
3737
| `fixed` | The vulnerability has been remediated in this version. DHI does not use this status (see below). |
3838

3939
You can view the VEX statements for any DHI using Docker Scout. See [Scan Docker
40-
Hardened Images](/manuals/dhi/how-to/scan.md).
40+
Hardened Images](/manuals/dhi/how-to/scan.md). To query suppressed CVEs
41+
programmatically by image digest, see [Query suppressed CVEs for a DHI by
42+
digest](/manuals/dhi/how-to/vex-api.md).
4143

4244
### `not_affected` justification codes
4345

content/manuals/dhi/how-to/_index.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ params:
4444
description: Learn how to scan Docker Hardened Images for known vulnerabilities using Docker Scout, Grype, or Trivy.
4545
icon: bug-ant
4646
link: /dhi/how-to/scan/
47+
- title: Query VEX for a Docker Hardened Image
48+
description: Use the DHI GraphQL API to fetch VEX statements and suppressed CVEs for a Docker Hardened Image by digest, at scale.
49+
icon: bug-ant
50+
link: /dhi/how-to/vex-api/
4751
grid_govern:
4852
- title: Apply Docker Hardened Image policies to your images
4953
description: Learn how to hold your own images to Docker Hardened Image security and compliance standards using the Docker Scout CLI.
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
---
2+
title: Query VEX for a Docker Hardened Image
3+
linktitle: Query VEX suppressions via API
4+
description: Use the DHI GraphQL API to fetch VEX statements and suppressed CVEs for a Docker Hardened Image by digest.
5+
keywords: dhi vex api, graphql api, suppressed cves, base vex, image digest, docker hardened images
6+
weight: 47
7+
---
8+
9+
This is a guided example of using the [DHI API](/manuals/dhi/tools/api.md)
10+
to fetch VEX statements and suppressed CVEs for a Docker Hardened Image (DHI)
11+
by digest, using the `imagePackagesForImageCoords` query. The query takes an
12+
image digest and returns every package, every CVE reported against it,
13+
whether Docker suppresses that CVE, and why.
14+
15+
> [!NOTE]
16+
> For one-off lookups or local scanning workflows, [`docker scout vex
17+
> get`](/manuals/dhi/how-to/scan.md#export-vex-attestations) may be simpler.
18+
> Use the API described here when you need to query many images
19+
> programmatically.
20+
21+
For the API endpoint and authentication, see [Use the DHI
22+
API](/manuals/dhi/tools/api.md). This page assumes you already have a valid
23+
token.
24+
25+
## Query suppressed CVEs
26+
27+
This is the `imagePackagesForImageCoords` query used in this example. It
28+
fetches every package in an image, every CVE reported against it, and
29+
whether Docker suppresses that CVE:
30+
31+
```graphql
32+
query BaseVex($ctx: Context!, $q: IpImagePackagesForImageCoordsQuery!) {
33+
imagePackagesForImageCoords(context: $ctx, query: $q) {
34+
imagePackages {
35+
packages {
36+
package {
37+
purl
38+
name
39+
version
40+
vulnerabilities {
41+
sourceId
42+
isExcepted
43+
fixedBy
44+
cvss {
45+
severity
46+
}
47+
vulnerabilityExceptions {
48+
id
49+
sourceType
50+
type
51+
justification
52+
additionalDetails
53+
isDhiStatement
54+
}
55+
}
56+
}
57+
}
58+
}
59+
}
60+
}
61+
```
62+
63+
With variables:
64+
65+
```json
66+
{
67+
"ctx": { "organization": "your-org" },
68+
"q": {
69+
"digest": "sha256:<platform-manifest-digest>",
70+
"hostName": "hub.docker.com",
71+
"repoName": "your-org/your-repo",
72+
"includeExcepted": true,
73+
"includeNodsa": true
74+
}
75+
}
76+
```
77+
78+
For the full argument reference, see
79+
[`imagePackagesForImageCoords`](/manuals/dhi/tools/api.md#imagepackagesforimagecoords).
80+
81+
### Get an access token
82+
83+
Exchange your organization access token (OAT) or personal access token (PAT)
84+
for an access token, as described in [Use the DHI
85+
API](/manuals/dhi/tools/api.md#authentication):
86+
87+
```console
88+
$ DHI_API_TOKEN=$(curl -s -X POST https://hub.docker.com/v2/auth/token \
89+
-H "Content-Type: application/json" \
90+
-d "{\"identifier\": \"<identifier>\", \"secret\": \"<token>\"}" \
91+
| jq -r .access_token)
92+
```
93+
94+
Use your Docker Hub username as `identifier` for a PAT, or your organization
95+
name for an OAT.
96+
97+
### Send the request
98+
99+
Combine the query and variables into the request body described in [Use the
100+
DHI API](/manuals/dhi/tools/api.md#request-format). The following example
101+
uses `jq` to build the body safely, since the query spans multiple lines:
102+
103+
```console
104+
$ QUERY='query BaseVex($ctx: Context!, $q: IpImagePackagesForImageCoordsQuery!) {
105+
imagePackagesForImageCoords(context: $ctx, query: $q) {
106+
imagePackages {
107+
packages {
108+
package {
109+
purl
110+
name
111+
version
112+
vulnerabilities {
113+
sourceId
114+
isExcepted
115+
fixedBy
116+
cvss {
117+
severity
118+
}
119+
vulnerabilityExceptions {
120+
id
121+
sourceType
122+
type
123+
justification
124+
additionalDetails
125+
isDhiStatement
126+
}
127+
}
128+
}
129+
}
130+
}
131+
}
132+
}'
133+
134+
$ curl https://api.dso.docker.com/v1/graphql \
135+
-H "Authorization: Bearer $DHI_API_TOKEN" \
136+
-H "Content-Type: application/json" \
137+
-d "$(jq -n --arg query "$QUERY" '{
138+
query: $query,
139+
variables: {
140+
ctx: { organization: "your-org" },
141+
q: {
142+
digest: "sha256:<platform-manifest-digest>",
143+
hostName: "hub.docker.com",
144+
repoName: "your-org/your-repo",
145+
includeExcepted: true,
146+
includeNodsa: true
147+
}
148+
}
149+
}')"
150+
```
151+
152+
Substitute your organization, digest, host, and repository.
153+
154+
## Sample response
155+
156+
The following example is trimmed to a single package:
157+
158+
```json
159+
{
160+
"package": {
161+
"purl": "pkg:deb/debian/tar@1.35%2Bdfsg-3.1%2Bdhi1?os_distro=trixie&os_name=debian&os_version=13",
162+
"name": "tar",
163+
"version": "1.35+dfsg-3.1+dhi1",
164+
"vulnerabilities": [
165+
{
166+
"sourceId": "CVE-2025-45582",
167+
"cvss": { "severity": "MEDIUM" },
168+
"fixedBy": null,
169+
"isExcepted": false,
170+
"vulnerabilityExceptions": []
171+
},
172+
{
173+
"sourceId": "CVE-2026-18477",
174+
"cvss": { "severity": "MEDIUM" },
175+
"fixedBy": null,
176+
"isExcepted": true,
177+
"vulnerabilityExceptions": [
178+
{
179+
"id": "debian-nodsa-CVE-2026-18477",
180+
"sourceType": "EXTERNAL",
181+
"type": "FALSE_POSITIVE",
182+
"justification": null,
183+
"additionalDetails": "Debian NODSA",
184+
"isDhiStatement": false
185+
}
186+
]
187+
}
188+
]
189+
}
190+
}
191+
```
192+
193+
Use `isExcepted` to tell whether a CVE is suppressed. For the full response
194+
field reference and the OpenVEX mapping, see
195+
[`imagePackagesForImageCoords`](/manuals/dhi/tools/api.md#imagepackagesforimagecoords).
196+
197+
## Query customization images
198+
199+
DHI VEX statements apply directly to images built with the [customization
200+
feature](/manuals/dhi/how-to/customize.md). Query the customized image's own
201+
digest to get its packages, CVEs, and suppressions in a single call. You
202+
don't need to query the base image separately.
203+
204+
## Caveats
205+
206+
The suppression set for a given digest can change as new advisories and
207+
assessments are published. Cache results per digest with a short TTL rather
208+
than treating a response as permanent.

content/manuals/dhi/release-notes/platform.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ New features and enhancements released in the third quarter of 2026.
2020
repositories, inspect image metadata, retrieve SBOMs, check CVEs, and manage
2121
mirrors using plain language. For more information, see [Use the DHI MCP
2222
server](/dhi/tools/mcp/).
23+
- DHI API: VEX statements and suppressed CVEs for a Docker Hardened Image are
24+
now available to query by digest through the DHI GraphQL API. For more
25+
information, see [Use the DHI API](../tools/api.md).
2326

2427
## Q2 2026
2528

content/manuals/dhi/tools/_index.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ params:
2020
description: Use the DHI Terraform provider to manage mirrors and automate DHI configuration as infrastructure as code.
2121
icon: wrench-screwdriver
2222
link: /dhi/tools/terraform/
23+
- title: Use the DHI API
24+
description: Query Docker Hardened Images data programmatically using the DHI GraphQL API.
25+
icon: code-bracket
26+
link: /dhi/tools/api/
2327
---
2428

2529
Docker Hardened Images can be accessed and managed through several interfaces.

content/manuals/dhi/tools/api.md

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
---
2+
title: Use the DHI API
3+
linktitle: API
4+
description: Query Docker Hardened Images data programmatically using the DHI GraphQL API.
5+
weight: 50
6+
keywords: dhi api, docker hardened images api, graphql api, dhi endpoint, dhi authentication
7+
---
8+
9+
The DHI API is a GraphQL API for querying Docker Hardened Images data
10+
programmatically, for use cases like building automation or dashboards on
11+
top of DHI data.
12+
13+
## Endpoint
14+
15+
Send requests as `POST` requests to:
16+
17+
```text
18+
https://api.dso.docker.com/v1/graphql
19+
```
20+
21+
## Request format
22+
23+
The API accepts standard GraphQL requests: a JSON body with a `query` and,
24+
optionally, `variables`.
25+
26+
```console
27+
$ curl https://api.dso.docker.com/v1/graphql \
28+
-H "Authorization: Bearer <token>" \
29+
-H "Content-Type: application/json" \
30+
-d '{"query": "...", "variables": { ... }}'
31+
```
32+
33+
Every query takes a `Context` argument (conventionally named `ctx` in the
34+
`variables` object) alongside its query-specific arguments:
35+
36+
| Argument | Type | Required | Description |
37+
|---|---|---|---|
38+
| `ctx` | `Context` | Yes | Scopes the request to an organization. |
39+
| `ctx.organization` | `String` | Yes | The Docker organization the token belongs to. |
40+
41+
## Authentication
42+
43+
An [organization access token](/manuals/enterprise/security/access-tokens.md)
44+
(OAT) or personal access token (PAT) isn't used directly as the bearer
45+
token. Exchange it first for an access token:
46+
47+
```console
48+
$ curl -X POST https://hub.docker.com/v2/auth/token \
49+
-H "Content-Type: application/json" \
50+
-d '{"identifier": "<identifier>", "secret": "<token>"}'
51+
```
52+
53+
For `identifier`, use your Docker Hub username with a PAT, or the
54+
organization name with an OAT. The response contains the access token:
55+
56+
```json
57+
{ "access_token": "..." }
58+
```
59+
60+
Pass that `access_token` as `Authorization: Bearer <access_token>`. Also set
61+
`ctx.organization` in `variables` to the organization the token belongs to
62+
(see [Request format](#request-format)).
63+
64+
## Response format
65+
66+
Responses follow the standard GraphQL envelope:
67+
68+
| Key | Description |
69+
|---|---|
70+
| `data` | The requested fields. A field is `null` if it couldn't be resolved, for example due to an authorization failure. |
71+
| `errors` | Present when a field failed to resolve. Includes a `message` and a `path` identifying which field failed. |
72+
| `extensions` | Metadata such as a `correlation_id`, useful when reporting an issue. |
73+
74+
For example, an unauthenticated request, or a request for data your token
75+
can't access, returns a `null` result under `data` alongside an authorization
76+
error in `errors`, rather than an HTTP-level failure:
77+
78+
```json
79+
{
80+
"errors": [
81+
{
82+
"message": "You are not allowed to read data for this team",
83+
"path": ["someQuery"],
84+
"extensions": { "code": "DOWNSTREAM_SERVICE_ERROR", "status": 403 }
85+
}
86+
],
87+
"data": { "someQuery": null },
88+
"extensions": { "correlation_id": "..." }
89+
}
90+
```
91+
92+
## Queries
93+
94+
### `imagePackagesForImageCoords`
95+
96+
Fetches every package in an image, every CVE reported against it, and
97+
whether Docker suppresses that CVE, by digest. See [Query VEX for a Docker
98+
Hardened Image](/manuals/dhi/how-to/vex-api.md) for a guided example.
99+
100+
| Argument | Type | Required | Description |
101+
|---|---|---|---|
102+
| `digest` | `String` | Yes | The image's platform manifest digest, not the multi-arch index digest. |
103+
| `hostName` | `String` | Yes | `hub.docker.com` or `docker.io`. |
104+
| `repoName` | `String` | Yes | Repository name, with or without the namespace prefix. |
105+
| `includeExcepted` | `Boolean` | No | Include suppressed CVEs in the response alongside the reason for suppression. Without it, the response only shows the netted list, with no visibility into what was suppressed. |
106+
| `includeNodsa` | `Boolean` | No | Include Debian NODSA exclusions, which make up most suppressions on a Debian-based image. |
107+
| `includePublic` | `Boolean` | No | Also include public images when `ctx.organization` scopes the request to an organization. Not needed for a typical lookup. |
108+
109+
Keep the requested response fields limited to what you plan to render.
110+
Fields such as `locations`, `description`, `vulnerableRange`, and `epss`
111+
increase response size substantially and aren't needed for a CVE-count or
112+
suppressed-CVE view.
113+
114+
#### Response fields
115+
116+
`vulnerabilityExceptions` only contains records that actually suppress a
117+
CVE, so it always lines up with `isExcepted`: an empty array means the CVE
118+
is live. Use `isExcepted` as your filter for "is this CVE suppressed."
119+
120+
| Field | Meaning |
121+
|---|---|
122+
| `isExcepted` | Docker suppresses this CVE for this image. Use this to filter. |
123+
| `sourceType` | `EXTERNAL` (Debian NODSA), `MANUAL_EXCEPTION` (Docker analyst exception), or `VEX_STATEMENT` (an ingested VEX document). |
124+
| `type` | `FALSE_POSITIVE` and `ACCEPTED_RISK` suppress the CVE. `UNDER_INVESTIGATION` and `AFFECTED` don't. |
125+
| `justification` | The OpenVEX justification value. Always `null` for NODSA exclusions. |
126+
| `additionalDetails` | Free-text rationale for the suppression. |
127+
| `isDhiStatement` | Whether the statement is inherited from the DHI base image. |
128+
| `id` | Stable identifier for the statement. |
129+
130+
#### Mapping to OpenVEX
131+
132+
If your pipeline consumes OpenVEX documents (for example, Trivy's `--vex`
133+
flag), each suppressed record maps as follows:
134+
135+
| OpenVEX field | Source |
136+
|---|---|
137+
| `vulnerability.name` | `sourceId` |
138+
| `products[].@id` | The parent package's `purl` |
139+
| `status` | `not_affected` (from `type: FALSE_POSITIVE`) |
140+
| `justification` | `justification`, defaulting to `vulnerable_code_cannot_be_controlled_by_adversary` for NODSA exclusions |
141+
| `status_notes` | `additionalDetails` |
142+
| `@id` | `id` |

0 commit comments

Comments
 (0)