Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 66 additions & 26 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -109,42 +109,78 @@ cosign verify "$IMG" \
--certificate-identity-regexp='.*' \
--certificate-oidc-issuer-regexp='.*'

# NOTE: this will show three high findings that the gate does not. See below.
grype "$IMG"
```

On tag builds the SPDX document is also attached to the GitHub release. That copy is
a convenience for reading the inventory without a registry client — **the registry
attestation is the source of truth**, and the two can drift.

### The CVE gate reports, it does not block — and this image is why
### Scanning this image yourself shows three findings the gate does not

**This is the one thing to know before scanning this image locally.** A plain
`grype "$IMG"` reports **three high-severity CVEs**. The release gate reports zero and
passes. Neither number is wrong, and the difference is not drift:

```bash
IMG=ghcr.io/kevinpinscoe/python-docker-fun:latest

grype "$IMG" # 3 high findings
grype "$IMG" --vex .vex/openvex.json # 0 — what the gate actually sees
```

The three are dispositioned in an OpenVEX document at `.vex/openvex.json`, committed in
this repository and reviewable. The workflow passes it to Grype's `vex:` input; your
local run does not unless you say so. Run the second command from a checkout of this
repo to reproduce the gate exactly.

They are the only suppressions here, and each is temporary by construction — every
statement names the CPython release that supersedes it:

| CVE | Where | Why it does not affect this image |
|---|---|---|
| CVE-2026-11940 | `tarfile` | Never imported; the image reads no archives. |
| CVE-2026-11972 | `tarfile` | Same. |
| CVE-2026-15308 | `html.parser` | Never imported; the server emits HTML and parses none. |

This was not assumed from reading the source. It was measured inside the image:
importing exactly the application's import set (`os`, `signal`, `threading`,
`datetime`, `http.server`) loads 116 modules, and neither `tarfile` nor `html.parser`
is among them. `html.parser` was checked explicitly because `http.server` *does* import
`html` for `escape` and could plausibly have dragged the parser in — it does not.

All three are fixed only in CPython 3.15.0b4/3.15.0, an unreleased major version, so no
base-image change removes them. Every alternative was built and scanned rather than
guessed: Alpine's `apk python3` ships 3.14.5 with 5 blocking findings,
`distroless/python3` is CPython 3.11 on Debian 12 with 55, and `3.15.0rc1` still leaves
one while shipping a release candidate to a public registry. **When the base reaches
3.15 stable, delete `.vex/openvex.json`** — the gate then stands on a genuinely empty
baseline.

### The CVE gate blocks

The release workflow scans the pushed digest with Grype at `severity-cutoff: high`,
uploads the result to the **Security** tab, and does **not** fail the build.
uploads the result to the **Security** tab, and **fails the build** on any unsuppressed
finding at or above high. One is enough — `fail-build: true` is not a count threshold.

**The base image was changed to `python:3.14-slim` on 2026-08-06, and that did most
of the work:**
Getting to a blockable baseline took two base-image changes, each measured:

| | Findings | High + Critical |
| Base | Findings | High + Critical |
|---|---|---|
| `python:3.14` (until 2026-08-05) | 1,949 | 444 |
| `python:3.14-slim` (2026-08-06 →) | **177** | **30** |

`python_loop_output.py` imports nothing outside the standard library, so the full
Debian image was contributing a compiler toolchain, dev headers and assorted
libraries this container never executes — and every CVE in them. Roughly 91% of the
findings, and 93% of the blocking ones, were removed by deleting software that was
never used.
| `python:3.14-slim` (2026-08-06) | 177 | 30 |
| `python:3.14-alpine` (2026-08-07 →) | **12** | **3, all suppressed → 0** |

**30 blocking findings is still not zero**, so the gate stays warn-only for now — but
it is now in the same range as the other two images in this fleet (29 and 44) rather
than an order of magnitude worse. A distroless Python would cut it further; the
remaining findings are in the Debian slim base, not in anything this repo wrote.
`python_loop_output.py` imports nothing outside the standard library, so the full Debian
image was contributing a compiler toolchain, dev headers and assorted libraries this
container never executes — and every CVE in them. Almost all of the reduction came from
deleting software that was never used, not from waiving anything.

When that residue is triaged, flip `fail-build` to `true` in
`.github/workflows/build.yaml`. Do not raise `severity-cutoff` to make findings
disappear, and do not delete the step. A CVE that genuinely does not affect this
image is dispositioned with an OpenVEX statement at `.vex/openvex.json`, committed
and reviewable — there is none today because nothing is being blocked.
Do not raise `severity-cutoff` to make findings disappear, and do not delete the scan
step. A CVE that genuinely does not affect this image is dispositioned with an OpenVEX
statement, committed and reviewable, and only with evidence that the vulnerable code is
unreachable.

### What the SBOM does not tell you

Expand All@@ -156,14 +192,18 @@ An SBOM is an inventory, not a clean bill of health. For this image specifically
`BUILDKIT_SBOM_SCAN_STAGE=true` or their dependencies vanish from the attestation.
- **`python_loop_output.py` is `COPY`'d in** without package metadata and will not
appear as a component.
- **Being listed is not being reachable.** The remaining findings are in Debian slim
base packages this container never executes.
- **Being listed is not being reachable.** That is the entire basis of the three VEX
statements above: the vulnerable code ships inside CPython's standard library and is
never loaded by this application.
- **Generators disagree.** Two SBOMs of this image, from different tools, will not
match line for line.

**Known gap:** this image is built for the runner's platform only (`linux/amd64`),
so unlike the multi-arch images in this fleet there is no arm64 variant to scan —
but equally, an arm64 consumer has nothing to pull.
**Single-platform, by design.** This image is built for `linux/amd64` only, so unlike
the multi-arch images in this fleet there is no arm64 variant to scan — and equally, an
arm64 consumer has nothing to pull. The sibling repos (`pastebooks`, `eng-tools`) are
multi-arch and scan each platform's digest separately, because Grype resolves a manifest
list to the runner's own platform and would otherwise check only one. That distinction
does not arise here.

**Rebuild trigger:** a digest's CVE posture is frozen at build time and only
degrades. Cut a new release when the base image updates.