From 073bb64aacc8b638d2dbe996ceda3a871f49b695 Mon Sep 17 00:00:00 2001 From: Dmitrii Creed Date: Mon, 18 May 2026 16:12:35 +0400 Subject: [PATCH 1/3] fix(deps): bump docs python 3.9 -> 3.12-slim + pin requests/urllib3 to patched versions Closes 3 transitive-dep advisories surfaced in Scorecard after PR #264 made the docs-build dep graph visible: - GHSA-gc5v-m9x4-r6x2 (CVE-2026-25645) - requests insecure temp file reuse - GHSA-mf9v-mfxr-j63j (CVE-2026-44432) - urllib3 decompression-bomb bypass - GHSA-qccp-gfcp-xxvc (CVE-2026-44431) - urllib3 cross-origin header leak Both patched versions (requests>=2.33.0, urllib3>=2.7.0) declare requires_python>=3.10, so the python:3.9.18 base no longer resolves them. Bumping the docs-build container to python:3.12-slim (SHA-pinned digest 401f6e1a67dad31a1bd78e9ad22d0ee0a3b52154e6bd30e90be696bb6a3d7461 - OCI image index covering amd64 / arm64 / arm/v5 / arm/v7 / 386 / ppc64le / riscv64 / s390x, verified via docker manifest inspect). - docs/requirements.in: added requests>=2.33.0 + urllib3>=2.7.0 floors - docs/requirements.txt: regenerated via pip-compile --generate-hashes in the new python:3.12-slim container. Resolves to requests==2.34.2 + urllib3==2.7.0 with sha256 pins on every transitive. Side-effects from the Python bump: importlib-metadata + zipp removed (no longer needed on Python 3.10+). - push.yaml: bump python:3.9.18 -> python:3.12-slim SHA-digest pin on the docs-build step. Smoke test: pip install --require-hashes -r requirements.txt && mkdocs --version succeeds in the new container. Risk: docs-build runs in an ephemeral container with no secrets / no production data, so the upstream CVEs were not actually exploitable in this codebase. Fix is for Scorecard hygiene + defence-in-depth. Expected Scorecard delta: Vulnerabilities 5 -> 8 (closes 3 advisories). Signed-off-by: Dmitrii Creed --- .github/workflows/push.yaml | 10 +++++++++- docs/requirements.in | 10 ++++++++++ docs/requirements.txt | 31 ++++++++++++------------------- 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index e7c0ad4b..81347629 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -362,7 +362,15 @@ jobs: # docs/requirements.txt (generated via `pip-compile --generate-hashes` # against docs/requirements.in). Closes Scorecard Pinned-Dependencies # warning on this line. - docker run --rm -v $PWD/docs:/docs -w /docs python:3.9.18 sh -c "pip install --require-hashes -r requirements.txt && PATH=\$PATH:~/.local/bin mkdocs build" + # + # python:3.12-slim, SHA-pinned digest. Required because the + # patched versions of `requests` (>=2.33.0) and `urllib3` + # (>=2.7.0) used to close GHSA-gc5v / -mf9v / -qccp transitive- + # dep advisories declare `requires_python: >=3.10`. Python 3.9 + # base would force keeping the vulnerable transitives. + # Digest is multi-platform (OCI image index) — covers amd64 + # / arm64 / arm/v5 / arm/v7 / 386 / ppc64le / riscv64 / s390x. + docker run --rm -v $PWD/docs:/docs -w /docs python@sha256:401f6e1a67dad31a1bd78e9ad22d0ee0a3b52154e6bd30e90be696bb6a3d7461 sh -c "pip install --require-hashes -r requirements.txt && PATH=\$PATH:~/.local/bin mkdocs build" - name: upload docs artifact uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: diff --git a/docs/requirements.in b/docs/requirements.in index 4f894161..4a44b120 100644 --- a/docs/requirements.in +++ b/docs/requirements.in @@ -2,3 +2,13 @@ mkdocs~=1.5.3 mkdocs-material~=9.4.8 mkdocs-material-extensions~=1.3 mkdocs-mermaid2-plugin~=1.1.1 + +# Transitive-dep CVE pins surfaced by Scorecard after PR #264: +# - GHSA-gc5v-m9x4-r6x2 (CVE-2026-25645) - requests < 2.33.0 (insecure temp file reuse) +# - GHSA-mf9v-mfxr-j63j (CVE-2026-44432) - urllib3 < 2.7.0 (decompression-bomb bypass) +# - GHSA-qccp-gfcp-xxvc (CVE-2026-44431) - urllib3 < 2.7.0 (cross-origin header leak) +# Pinning the patched floors here forces pip-compile to resolve to safe +# versions. Both deps are reachable through mkdocs-material's plugin +# chain at docs-build time only. +requests>=2.33.0 +urllib3>=2.7.0 diff --git a/docs/requirements.txt b/docs/requirements.txt index 4fb522fd..74756e76 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,8 +1,8 @@ # -# This file is autogenerated by pip-compile with Python 3.9 +# This file is autogenerated by pip-compile with Python 3.12 # by the following command: # -# pip-compile --allow-unsafe --generate-hashes --output-file=requirements.txt requirements.in +# pip-compile --allow-unsafe --cert=None --client-cert=None --generate-hashes --index-url=None --output-file=requirements.txt --pip-args=None requirements.in # babel==2.18.0 \ --hash=sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d \ @@ -167,12 +167,6 @@ idna==3.15 \ --hash=sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8 \ --hash=sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc # via requests -importlib-metadata==8.7.1 \ - --hash=sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb \ - --hash=sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151 - # via - # markdown - # mkdocs jinja2==3.1.6 \ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 @@ -553,10 +547,11 @@ regex==2026.1.15 \ --hash=sha256:fe2fda4110a3d0bc163c2e0664be44657431440722c5c5315c65155cab92f9e5 \ --hash=sha256:febd38857b09867d3ed3f4f1af7d241c5c50362e25ef43034995b77a50df494e # via mkdocs-material -requests==2.32.5 \ - --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ - --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf +requests==2.34.2 \ + --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ + --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed # via + # -r requirements.in # mkdocs-material # mkdocs-mermaid2-plugin six==1.17.0 \ @@ -573,10 +568,12 @@ typing-extensions==4.15.0 \ --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 # via beautifulsoup4 -urllib3==2.6.3 \ - --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ - --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 - # via requests +urllib3==2.7.0 \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 + # via + # -r requirements.in + # requests watchdog==6.0.0 \ --hash=sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a \ --hash=sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2 \ @@ -609,10 +606,6 @@ watchdog==6.0.0 \ --hash=sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c \ --hash=sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2 # via mkdocs -zipp==3.23.1 \ - --hash=sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc \ - --hash=sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110 - # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: setuptools==82.0.1 \ From dc71de7b7baacc9f65a6d2eac460af04288a34bb Mon Sep 17 00:00:00 2001 From: Dmitrii Creed Date: Mon, 18 May 2026 16:22:59 +0400 Subject: [PATCH 2/3] fix(fuzz): anchor expired-seed timestamps to fuzzNow, not real time.Now Codex round-2 P2 catch on PR #264: FuzzVerifyAndExtract seeds used time.Now().Add(-time.Hour) as the "expired" past timestamp, but the fuzz body itself uses a fixed clock at time.Unix(1_700_000_000) (Nov 2023) for deterministic expiry behaviour. Any real-clock "past" was years AFTER fuzzNow, so seed #3 was actually unexpired against the fuzz clock - never exercising the expiry-rejection branch. Mutator coverage still hit the path via random inputs, so no real coverage gap, but the named seed was misleading. Now anchored to a fuzzNow constant shared with the fuzz body. 5s fuzz smoke: 448k execs, 4 new interesting paths, zero panics. Signed-off-by: Dmitrii Creed --- pkg/security/cache_fuzz_test.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pkg/security/cache_fuzz_test.go b/pkg/security/cache_fuzz_test.go index 1b9ff8c6..28d596fe 100644 --- a/pkg/security/cache_fuzz_test.go +++ b/pkg/security/cache_fuzz_test.go @@ -84,14 +84,21 @@ func FuzzVerifyAndExtract(f *testing.F) { keyA := CacheKey{Operation: "sbom", ImageDigest: "sha256:A", ConfigHash: "h1"} keyB := CacheKey{Operation: "scan-grype", ImageDigest: "sha256:B", ConfigHash: "h2"} - future := time.Now().Add(time.Hour) - past := time.Now().Add(-time.Hour) + // Anchor seed timestamps to the SAME fixed clock the fuzz body uses + // (fuzzNow below), not the real wall-clock. Earlier version used + // `time.Now().Add(-time.Hour)` for the "expired" seed, but the fuzz + // body's `now` is fixed to time.Unix(1_700_000_000, 0) (Nov 2023); + // any real-clock "past" is years AFTER that, so the expired-seed + // branch was never exercised by seed #3. Codex round-2 P2 catch. + fuzzNow := time.Unix(1_700_000_000, 0).UTC() + future := fuzzNow.Add(time.Hour) + past := fuzzNow.Add(-time.Hour) // Seed 1: a known-good signed entry, requested with the matching key. f.Add(validSignedBytes(f, cache, keyA, []byte("payload-A"), future), "sbom", "sha256:A", "h1") // Seed 2: same valid bytes, requested with a DIFFERENT key — must miss. f.Add(validSignedBytes(f, cache, keyA, []byte("payload-A"), future), "scan-grype", "sha256:B", "h2") - // Seed 3: valid entry but already expired — must miss. + // Seed 3: valid entry but already expired vs fuzzNow — must miss. f.Add(validSignedBytes(f, cache, keyA, []byte("payload-A"), past), "sbom", "sha256:A", "h1") // Seed 4: a tampered Data payload (MAC no longer matches). tampered := tamperData(f, cache, keyA, []byte("orig"), []byte("evil"), future) From 732fc488f8eabdfcc032575cdd52bda72aacc957 Mon Sep 17 00:00:00 2001 From: Dmitrii Creed Date: Mon, 18 May 2026 17:45:53 +0400 Subject: [PATCH 3/3] fix(ci): dedup duplicate runs on push:[**] + pull_request via SHA concurrency key Codex round-2 P2 catch on PR #264. Both codeql.yml and fuzz.yml have: push: branches: ['**'] pull_request: branches: [main] A single PR commit triggers two runs: - push event: github.ref = refs/heads/ - pull_request event: github.ref = refs/pull/N/merge Previous concurrency group was keyed on github.ref, so these two runs landed in DIFFERENT groups and neither cancelled the other - ~2x the CI cost per PR commit. Switch to: group: ${{ github.workflow }}-${{ github.event.pull_request.head.sha || github.sha }} github.event.pull_request.head.sha resolves on pull_request events (the PR's head commit); github.sha resolves on push events (the pushed commit). For a single PR commit these are EQUAL, so both runs land in the same concurrency group and the second cancels the first. For pushes to main (no PR), the fallback github.sha keys by main's HEAD SHA - same behaviour as before for that case. Trade-off noted: under cancel-in-progress, the SECOND-arriving event cancels the FIRST. If push lands first then pull_request lands and cancels it, the surviving run executes under the pull_request event context (analysing the PR merge ref). If pull_request lands first, push survives (analyses branch HEAD). Either way the SAME commit SHA's source code is analysed, so SAST results are equivalent; context-derived metadata may differ. Signed-off-by: Dmitrii Creed --- .github/workflows/codeql.yml | 11 +++++++++-- .github/workflows/fuzz.yml | 8 ++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 00edf90b..cd097dcd 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -22,9 +22,16 @@ on: - cron: '23 4 * * 0' workflow_dispatch: -# Cancel in-progress runs for the same PR when a new commit lands. +# Cancel in-progress runs for the same commit when a new event lands. +# Key on commit SHA (PR head when triggered by pull_request, pushed SHA +# otherwise) instead of github.ref. With `push: branches: ['**']` and +# `pull_request: branches: [main]` both active, a single PR commit +# triggers two runs — one for the push event (refs/heads/) and +# one for the PR event (refs/pull/N/merge). Keying on ref leaves both +# in distinct groups, so neither cancels the other. SHA-keyed grouping +# dedups them. Codex round-2 P2 catch on PR #264. concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ${{ github.workflow }}-${{ github.event.pull_request.head.sha || github.sha }} cancel-in-progress: true permissions: diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index b8d332a4..1ac13386 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -39,9 +39,13 @@ on: - cron: '17 7 * * 1' workflow_dispatch: -# Cancel in-progress runs for the same PR when a new commit lands. +# Cancel in-progress runs for the same commit when a new event lands. +# SHA-keyed (PR head SHA or push SHA) instead of ref-keyed, so a single +# PR commit doesn't run twice — once for the push:[**] event and once +# for the pull_request event with different github.ref values. Same +# fix as codeql.yml. Codex round-2 P2 catch on PR #264. concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ${{ github.workflow }}-${{ github.event.pull_request.head.sha || github.sha }} cancel-in-progress: true permissions: