Skip to Content
Export an SBOM or Scan Result From a Real Build
Advanced
0 likes
Tutorials

Export an SBOM or Scan Result From a Real Build

Export an SPDX SBOM from a real build, list its packages and scan it with Grype, without a Docker account. After this, you can hand over what is in an image and what was known about it on a given day.

An SBOM (software bill of materials) is a list of every package inside an image, with versions. A scan result is that list compared with known vulnerabilities on a given day. Both are things a customer, an auditor or your own security team may ask for, and both are easy to produce from a real build. This page exports an SBOM from a build, lists it, and scans it, without a Docker account. Run on Docker Desktop 4.85.0 (Engine 29.6.2) with BuildKit, Docker Scout CLI 1.23.1 (only for the package list) and Grype 0.120.1 in a container (for the scan).

Build with an SBOM

requests==2.32.5
FROM python:3.14-alpine
COPY requirements.txt .
RUN pip install --no-cache-dir --root-user-action=ignore -r requirements.txt
CMD ["python", "-c", "import requests; print(requests.__version__)"]

--sbom=true tells BuildKit to scan the result and attach an SBOM in SPDX format. With --output type=tar, the image and its SBOM are written to one file instead of your image list.

Set-Location sbom-demo
docker buildx build -q --sbom=true --output type=tar,dest=image.tar . | Out-Null
tar -tf image.tar | Select-String "^sbom"
tar -xf image.tar sbom.spdx.json
(Get-Content sbom.spdx.json -Raw | ConvertFrom-Json).PSObject.Properties.Name

sbom.spdx.json
_type
predicateType
subject
predicate

The tar holds the whole image filesystem plus sbom.spdx.json at the top. That file is not plain SPDX yet. BuildKit wraps it as an in-toto attestation: predicate holds the SPDX document, and subject says which image it belongs to. Scanners expect the plain document, so I unwrap it into its own file, written as UTF-8 without a byte order mark:

$spdx = (Get-Content sbom.spdx.json -Raw | ConvertFrom-Json).predicate
[IO.File]::WriteAllText("$PWD\sbom.json", ($spdx | ConvertTo-Json -Depth 100 -Compress))
"SBOM size: {0:N0} KB" -f ((Get-Item sbom.json).Length / 1KB)
SBOM size: 620 KB

Keep both files with the build in your CI artifacts, together with the image digest, so you can always say what was in a given release.

On Windows, --output type=local,dest=folder failed for me with "A required privilege is not held by the client", because the image contains Linux symbolic links that Windows will not create without extra rights. The tar output avoids that.

What is in it

The file is JSON. PowerShell can count and list packages directly:

$sbom = Get-Content sbom.json -Raw | ConvertFrom-Json
"packages listed: " + $sbom.packages.Count
$sbom.packages | Where-Object { $_.name -in "requests", "urllib3", "python", "musl" } | Sort-Object name | ForEach-Object { "{0} {1}" -f $_.name, $_.versionInfo } | Select-Object -Unique
packages listed: 63
musl 1.2.6-r2
python 3.14.8
requests 2.34.2
requests 2.32.5
urllib3 2.8.0
urllib3 2.7.0

Both the Alpine system packages and the Python packages from pip install are in the list. requests and urllib3 each appear twice. The SBOM's own notes on each entry explain why: requests 2.32.5 and urllib3 2.8.0 are what pip install put in site-packages, while requests 2.34.2 and urllib3 2.7.0 come from the SBOM file that pip ships for the copies it bundles inside itself (pip/_vendor/bom.cdx.json). That is the point of building the SBOM from the real image instead of from requirements.txt: it also catches what the base image brought along.

If you prefer a table, docker scout sbom --format list IMAGE works without signing in for an image in your local list. Its vulnerability commands (docker scout cves, quickview) asked me to sign in with a Docker ID, so I used Grype for the scan below.

Scan the SBOM

Grype can read the SBOM file directly, so the scan does not even need the image:

docker run --rm -v "${PWD}:/work:ro" anchore/grype:v0.120.1 sbom:/work/sbom.json --only-fixed -o table -q 2>$null
"grype exit code: $LASTEXITCODE"
NAME        INSTALLED  FIXED IN   TYPE            VULNERABILITY        SEVERITY  EPSS         RISK
setuptools  70.3.0     78.1.1     python          GHSA-5rjg-fvgr-3xxf  High      1.6% (75th)  1.2
zlib        1.3.2-r0   1.3.2-r1   apk             CVE-2026-85091       High      0.4% (27th)  0.3
urllib3     2.7.0      2.8.0      python          GHSA-vxq7-64xx-v4gw  High      0.3% (25th)  0.3
setuptools  70.3.0     83.0.0     python          GHSA-h35f-9h28-mq5c  Medium    0.4% (33rd)  0.2
python      3.14.8     3.15.0a6   UnknownPackage  CVE-2025-15367       Medium    0.4% (30th)  0.2
urllib3     2.7.0      2.8.0      python          GHSA-gh4c-6fx4-qh6g  Medium    0.3% (20th)  0.2
urllib3     2.7.0      2.8.0      python          GHSA-8988-9cw3-xx77  High      0.2% (10th)  0.2
python      3.14.8     3.15.0rc3  UnknownPackage  CVE-2026-12345       Medium    0.2% (7th)   < 0.1
requests    2.32.5     2.33.0     python          GHSA-gc5v-m9x4-r6x2  Medium    0.2% (7th)   < 0.1
msgpack     1.1.2      1.2.1      python          GHSA-6v7p-g79w-8964  High      N/A          N/A
grype exit code: 0

The urllib3 2.7.0 findings are in pip's bundled copy, not in the version the app uses. That is still worth knowing: pip is in the image. Your result will not match mine, and it should not. The packages are fixed, but the vulnerability database changes every day. A scan result is only meaningful with its date and the scanner version. To keep one as a file:

docker run --rm -v "${PWD}:/work" anchore/grype:v0.120.1 sbom:/work/sbom.json -o json --file /work/scan-2026-10-21.json -q 2>$null
"scan saved: " + (Test-Path scan-2026-10-21.json)
scan saved: True

Make the build fail on serious findings

Grype can return a non-zero exit code when it finds something at or above a severity, which is what a CI step needs:

docker run --rm -v "${PWD}:/work:ro" anchore/grype:v0.120.1 sbom:/work/sbom.json --only-fixed --fail-on critical -q 2>$null | Out-Null
"exit code with --fail-on critical: $LASTEXITCODE"
exit code with --fail-on critical: 0

0 means nothing at that level with a fix available today. Choose the level you are ready to act on. A gate that fails every build gets switched off.

If it does not work

  • --sbom is ignored or fails: check that you build with BuildKit (docker buildx build), and that your builder supports attestations. Docker Desktop's default builder did here.
  • Grype is slow on the first run: it downloads its vulnerability database every time the container starts fresh. In CI, cache that folder.
  • An SBOM with almost nothing in it: a scratch image with one static binary has very few packages to list. Generate the SBOM for the build stage or from the source as well.

Related course: Docker Advanced.

About

Export an SPDX SBOM from a real build, list its packages and scan it with Grype, without a Docker account. After this, you can hand over what is in an image and what was known about it on a given day.

Responsible
Mary Q
Last Update
08/03/2026
Reading Time
6 min
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.