From d907cda8368126d77acd6c4c21aa96bdf63d47ff Mon Sep 17 00:00:00 2001 From: "Kevin P. Inscoe" Date: Sun, 9 Aug 2026 08:48:47 -0400 Subject: [PATCH] =?UTF-8?q?docs:=20lead=20with=20the=20VEX=20divergence=20?= =?UTF-8?q?=E2=80=94=20local=20scans=20show=203=20findings=20the=20gate=20?= =?UTF-8?q?does=20not?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is the only image in the fleet that suppresses anything, and the README described neither the suppressions nor the blocking gate. It still documented python:3.14-slim with 30 blocking findings and a warn-only gate, two base images out of date. The section now opens with the discrepancy a reader will actually hit: a plain `grype` run reports three high-severity CVEs while the gate reports zero and passes. It gives the exact command that reproduces the gate (--vex .vex/openvex.json), names all three CVEs and why the vulnerable code is unreachable, and states that they are superseded by CPython 3.15 so the document is temporary by construction. Measured against the published image while writing this: 12 findings total, 3 at high, 0 unsuppressed. Refs gitops PLAN.md item 11c, YouTrack GH-2. --- README.md | 92 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 66 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index aa3a17b..10e0105 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,7 @@ 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" ``` @@ -116,35 +117,70 @@ On tag builds the SPDX document is also attached to the GitHub release. That cop 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 @@ -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.