Skip to content

fix(deps): SCA pass — close all fixable vulns across source, images and CI pins - #377

Merged
Cre-eD merged 2 commits into
mainfrom
fix/sca-deps-2026-08
Aug 11, 2026
Merged

fix(deps): SCA pass — close all fixable vulns across source, images and CI pins#377
Cre-eD merged 2 commits into
mainfrom
fix/sca-deps-2026-08

Conversation

@Cre-eD

@Cre-eD Cre-eD commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Full SCA pass over source, all 5 published container images, and the CI pin surface. Consolidates the 7 open Dependabot PRs into one review and adds 5 findings none of them could see.

Fixed Deferred Documented unfixable
CRITICAL 0 0 0
HIGH 1 source + 102 image 0 8
MEDIUM 1 source + 32 image 0 4
LOW 0 + 1 image 0 0

Locally-built images, origin/main vs this branch, identical build inputs:

image trivy (main → branch) grype --only-fixed (main → branch)
kubectl 21 → 8 39 → 11
caddy 28 → 3 37 → 9
cloud-helpers 64 → 0 27 → 4
github-actions 34 → 1 44 → 5
total 147 → 12 (−92%) 147 → 29 (−80%)

Every one of the 12 remaining findings sits inside a third-party prebuilt binary already at its newest available version. Nothing fixable is left unfixed, and no suppression was added.

Read Round 2 at the bottom too. A six-model review panel plus two real preview builds followed this round; it corrected two defects in the diff below (cache scoping, and a weakened pin in welder.yaml) and found three pre-existing ones, including a Pulumi checksum gate that could not fail and a published image that cannot start.

Fixed

Package Old → New CVE / advisory Sev Source
github.com/go-git/go-git/v5 5.19.1 → 5.19.2 CVE-2026-71556 (GHSA-hc8v-wwc9-vgxm) worktree ops follow symlinks HIGH trivy fs, osv-scanner, Dependabot alert #75
github.com/go-git/go-git/v5 5.19.1 → 5.19.2 CVE-2026-71557 (GHSA-qgq7-7hm3-q39j) malicious ref names write outside ref storage MEDIUM trivy fs, osv-scanner, Dependabot alert #76
Caddy binary (xcaddy build) v2.11.3 → v2.11.4 CVE-2026-52844, CVE-2026-52845 (HIGH), CVE-2026-52846 (MEDIUM) H×2 M×1 trivy image
alpine base (×4 stages) 3.23 → 3.24 30 distro CVEs → 0; also lifts py3-cryptography 46.0.7 → 47.0.0 H2 M8 L20 trivy image
caddy:2.11.4 + -builder digest refresh 55 → 25 in the base layer H12 M23 L20 trivy image
alpine/kubectl:latest digest refresh 68 → 8 in the base layer H20 M28 L20 trivy image
lambda/provided:al2023 digest refresh 64 → 0 in the base layer H58 M5 L1 trivy image
gcloud CLI 567.0.0 → 579.0.0 rolls up vendored Go deps in the bundled binaries; SHA-256 recomputed from the real tarball trivy image
python:3.9.18python@sha256:229a2c5b (3.12-slim) (welder build-docs) EOL runtime 10110 → 213 image vulns (C248→4, H2293→23) C/H/M/L trivy image

CI pin bumps

actions/checkout v6.0.3 → v7.0.1 (Dependabot proposed v7.0.0; v7.0.1 adds the --unset escaping and default-input fixes) · actions/cache + actions/cache/save v5.0.5 → v6.1.0 · actions/setup-go v6 → v7.0.0 · actions/attest-build-provenance v4.1.0 → v4.2.2 · docker/setup-buildx-action v4.1.0 → v4.2.0 · docker/build-push-action v7.2.0 → v7.3.0 · ossf/scorecard-action v2.4.3 → v2.4.4 · github/codeql-action v4 SHA refresh (comment corrected to # v4.37.6 in round 2).

All SHAs resolved through the GitHub git-ref API, annotated tags dereferenced to commits. Compatibility checked before applying:

  • ESM majors are safe here. checkout v7, cache v6 and setup-go v7 are all "migrate to ESM" releases. Both the old and new versions declare using: node24, so no runner requirement changes on blacksmith-8vcpu-ubuntu-2204, ubuntu-24.04 or ubuntu-latest.
  • checkout v7's fork-PR block does not bite. v7 refuses to check out fork PR code under pull_request_target / workflow_run unless allow-unsafe-pr-checkout is set. This repo has no pull_request_target at all; the three workflow_run workflows either do not check out (security-scan-comment.yml, semgrep-comment.yml) or check out the base repo with no ref: (verify-attestations.yml). The one PR-head checkout (dco.yml, ref: github.event.pull_request.head.sha) runs on pull_request, which the block does not cover.

Stale pin comment

build-staging.yml pinned actions/cache@27d5ce7f with a # v4.3.0 comment, while push.yaml annotated the same SHA as # v5.0.5. The GitHub tag API confirms 27d5ce7f is v5.0.5; the v4.3.0 comment was wrong and made the pin look two majors older than it was. Corrected as part of the bump.

Two findings Dependabot could not see

1. caddy.Dockerfile shipped a binary one patch behind its own base image

The file header says "Bumping requires editing all three '2.11.x' sites below (two FROMs + xcaddy)". Both FROM lines were on caddy:2.11.4, but xcaddy build "v2.11.3" was left behind, and the built binary is what COPY --from=builder /usr/bin/caddy puts in the final image. So the shipped Caddy was 2.11.3 and still carried CVE-2026-52844 + CVE-2026-52845. Dependabot tracks FROM lines, not xcaddy arguments, so this class of drift is invisible to it. Verified after the fix:

$ docker run --rm --entrypoint sh <built> -c 'cat /etc/alpine-release; caddy version; caddy list-modules | grep -E "rate_limit|gcs"'
3.23.5
v2.11.4 h1:XKxkMTgNSizEvKG6QHue6cAsFOteU2qA61w2tKkCWi0=
caddy.storage.gcs
http.handlers.rate_limit

2. Published images do not match the Dockerfiles that supposedly built them

simplecontainer/caddy:latest is byte-identical to :2026.8.3 (both sha256:83b0e6bb…, pushed 2026-08-10 11:52 UTC, today). What is inside it:

$ docker run --rm --entrypoint sh simplecontainer/caddy:latest -c \
    'cat /etc/alpine-release; caddy version; ls -la /usr/bin/caddy; caddy list-modules | grep -E "rate_limit|gcs"'
3.20.4
v2.8.4 h1:q3pe0wpBj1OcHFZ3n/1nl4V4bxBrYoSoab7rL9BMYNk=
-rwxr-xr-x 1 root root 41582744 Apr 27 08:56 /usr/bin/caddy
caddy.storage.gcs

Three things are wrong at once: alpine 3.20.4 where the pinned base resolves to 3.23.5, Caddy v2.8.4 where the Dockerfile builds 2.11.x, a /usr/bin/caddy timestamped Apr 27, and http.handlers.rate_limit absent. That last one is decisive: caddy.Dockerfile asserts on that module at build time (caddy list-modules | grep -qE '^http\.handlers\.rate_limit$'), so the published image cannot be the output of that Dockerfile. Its apk database also reports alpine 3.20.4 while a fresh build of the same file reports 3.23.5.

Cost of that in the artifact consumers actually pull (trivy image, published :latest tags):

published image C H M L total
simplecontainer/caddy:latest 6 69 68 27 170
simplecontainer/github-actions:latest 2 67 51 8 128
simplecontainer/cloud-helpers:aws-latest 0 73 13 1 87
simplecontainer/kubectl:latest 0 21 38 24 83

Among caddy's 6 CRITICALs: smallstep/certificates v0.26.1 (CVE-2025-44005, CVE-2026-30836), grpc v1.64.0 (CVE-2026-33186), libcrypto3/libssl3 3.3.2-r1 (CVE-2026-31789), Go stdlib v1.23.4 (CVE-2025-68121) — plus the four Caddy admin-socket / fastcgi auth-bypass HIGHs (CVE-2026-27586/27587/27588/27590) that caddy.Dockerfile's own header claims to have closed. This matters more than usual because the SC composite actions run these images by floating tag rather than digest, so consumers inherit whatever :latest points at.

Probable mechanism, and the change here. Every docker/build-push-action step in the repo used cache-from: type=gha / cache-to: type=gha,mode=max with no scope:, so all builds shared one cache namespace: the 5-way image matrix in push.yaml (all writing mode=max concurrently), both images in build-staging.yml, and the preview matrix in branch-preview.yaml. Concurrent mode=max writers into a single GHA cache scope is a known way to get cache-index collisions across unrelated Dockerfiles. This PR scopes the cache per build — see Round 2 below, which corrects the first attempt at this: scoping per image still let push.yaml and branch-preview.yaml collide on github-actions, so the final design keys scopes by trust tier.

I want to be precise about confidence: the observation above is proven by direct execution, and unscoped shared cache is the most plausible mechanism, but I have not reproduced the collision inside GitHub's cache backend. If the next release still publishes a stale caddy after this lands, the next place to look is the welder image-build path rather than buildx. Either way scoping the cache is correct on its own merits.

Documented unfixable (12 remaining, 0 CRITICAL)

Every residual finding lives in a third-party prebuilt binary that we do not compile, each already at the newest published version. No .trivyignore, no VEX statement added for any of these — a scanner finding against a current upstream artifact is honest signal and should keep showing until upstream ships.

/usr/local/bin/kubectl (8) — upstream Kubernetes v1.36.3. golang.org/x/net v0.49.0 (CVE-2026-25681, CVE-2026-27136, CVE-2026-39821, CVE-2026-33814 HIGH; CVE-2026-25680, CVE-2026-42502, CVE-2026-42506 MEDIUM) and golang.org/x/text v0.33.0 (CVE-2026-56852). Reachability: kubectl here is a CI-driven CLI speaking TLS to a trusted, authenticated API server endpoint; the x/net HTTP/2 and proxy parsing paths need a hostile server or hostile URL, and the x/text issue is an infinite loop on invalid input (DoS of a short-lived CI process). alpine/kubectl:latest is already the newest digest. Closes when: upstream k8s rebuilds against x/net ≥ 0.55.0.

/usr/bin/caddy (3) — vendored into Caddy v2.11.4's own module graph. x/text v0.37.0 (CVE-2026-56852), grpc v1.81.0 (GHSA-hrxh-6v49-42gf, xDS RBAC fail-open + HTTP/2), cel-go v0.28.1 (GHSA-gcjh-h69q-9w9g, json:"-" fields readable from CEL). Reachability: Caddy is genuinely internet-facing, so these deserve the least benefit of the doubt — but both non-x/text findings sit in features this build does not use. The grpc advisory is scoped to internal/xds/rbac and the HTTP/2 server transport; Caddy links grpc as a client for its own internals and has no xDS RBAC authorization engine. cel-go is reached only through ext.NativeTypes(ParseStructTag("json")) over user-submitted CEL, which requires a CEL expression surface this build does not expose (the plugin set is certmagic-gcs + caddy-ratelimit). Overriding Caddy's module graph via xcaddy would mean shipping a Caddy built against dependency versions upstream has not tested, which trades a narrow unreachable finding for a broad untested one. Closes when: Caddy 2.11.5+ ships.

/opt/google-cloud-sdk/bin/gke-gcloud-auth-plugin (1) — Google-built, gcloud 579.0.0 (newest). x/text v0.38.0, CVE-2026-56852. Reachability: a short-lived credential helper parsing Google's own token responses, not attacker-supplied text; the advisory is an infinite loop on invalid input. Closes when: Google rebuilds the plugin against x/text ≥ 0.39.0.

/usr/local/bin/aws-lambda-rie (4, grype only) — AWS-built, part of lambda/provided:al2023. x/sys v0.21.0 (GO-2026-5024), go-chi/chi/v5 v5.2.4 (GO-2026-5774/5775/5777). Reachability: the Runtime Interface Emulator is a local-testing shim AWS ships in the base image; it is not the entrypoint (ENTRYPOINT ["/cloud-helpers"]) and never runs in Lambda. Closes when: AWS refreshes the base image.

py3-cryptography 47.0.0 (4, grype only) — Alpine's newest, in github-actions. GHSA-537c-gmf6-5ccf, GHSA-jwv3-5hgf-82ww, GHSA-g6cj-pr64-35w5 (HIGH), GHSA-m2h6-j472-rp4c (MEDIUM). The fixes need 48.0.1 / 49.0.0 / 50.0.0, and Alpine ships 47.0.0-r0 on both 3.24 and edge — there is no branch to bump to. Reachability: pulled in by apk-managed aws-cli and used by gcloud auth; the PKCS#7 Bleichenbacher oracle and the permittedSubtrees wildcard escape need attacker-controlled PKCS#7 blobs or hostile certificate chains, neither of which is on the CI credential path. Why not force it: pip install cryptography>=48 over the apk package would break Alpine's externally-managed-environment marker and desynchronise the aws-cli/gcloud dependency set — worse than the finding. The 3.23 → 3.24 bump in this PR already moved it 46.0.7 → 47.0.0. Closes when: Alpine packages 48.0.1+.

Note this is a real trivy/grype split worth knowing: trivy scores py3-cryptography as an Alpine package with no distro fix and stays quiet, grype catalogues the same files as a PyPI dist-info and matches the GHSA advisories. Neither is wrong. Both tools are run here for exactly this reason.

VEX re-triage

vex/openvex.json v2 → v3. Nothing was added; two statements were corrected and one re-confirmed.

advisory was now why
GO-2022-0635 (CVE-2020-8911) fixed not_affected / component_not_present see below
GO-2022-0646 (CVE-2020-8912) fixed not_affected / component_not_present see below
GO-2026-5932 (x/crypto/openpgp) not_affected / vulnerable_code_not_present unchanged, evidence re-verified grep and call-path evidence still hold; x/crypto v0.54.0 is the latest release

The two fixed statements were not fixable, and that made them indefinite suppressions. OSV lists both aws-sdk-go advisories with {"introduced": "0"} and no fix event — AWS never patched the v1 s3crypto client, the remediation is migrating to the V2 encryption client. So fixed described a state that could never be reached, while still suppressing the finding in any VEX-aware scanner. The prior impact_statement also asserted the module had "left go.mod entirely through Go module pruning", which is not what the tree shows:

$ go list -m all | grep '^github.com/aws/aws-sdk-go '
github.com/aws/aws-sdk-go v1.55.8              # present in the requirement graph
$ go list -deps ./... | grep -c '^github.com/aws/aws-sdk-go/'
0                                              # zero packages linked into any binary
$ go mod why -m github.com/aws/aws-sdk-go
(main module does not need module github.com/aws/aws-sdk-go)

component_not_present is the accurate justification: the module is a requirement-graph edge from a transitive go.mod, no package from it reaches the linker, and s3crypto specifically is never imported (S3 goes through aws-sdk-go-v2). Neither govulncheck -mode=source nor osv-scanner reports these against the repo, so no osv-scanner.toml mirror entry is needed; the header comment there is updated to say why they stay out and under what condition they would come back.

osv-scanner.toml keeps exactly one IgnoredVulns entry (GO-2026-5932), unchanged. CODEOWNERS already gates /vex/ and /osv-scanner.toml.

Dependabot reconciliation

All 7 open bot PRs are superseded by this one. Note .github/dependabot.yml is deliberately configured so majors fall outside the version groups and open individual PRs "for isolated review/testing" — that config is working as designed and is left untouched; this PR is the isolated review.

PR What it proposed Action Why
#374 go-git/v5 5.19.1 → 5.19.2 (gomod-security) superseded identical bump; also closes Dependabot alerts #75/#76
#371 github-actions group, 8 updates superseded all 8 included, same SHAs
#352 actions/cache 5.0.5 → 6.1.0 superseded included; also fixes the stale # v4.3.0 comment it would have left behind
#351 actions/cache/save 5.0.5 → 6.1.0 superseded included. Its checks were red (SBOM + Dependency Scan, Security Status) on the go-git baseline, which this PR clears
#334 actions/checkout 6.0.3 → 7.0.0 superseded, improved went to v7.0.1 instead; v7.0.0 predates the --unset escaping fix
#333 alpine 3.23 → 3.24 superseded same digest 28bd5fe8…
#326 caddy digest f2b9891ea6e54f superseded, improved that digest was already stale (opened Jun 15; upstream rebuilt Jul 7). Used the current 198d47ea… / 844f60b6… instead

Two Dependabot alerts (#75 HIGH, #76 MEDIUM) are the only open ones and both close on merge.

Follow-ups, not done here

  • reecetech/version-increment is pinned at 2024.10.1; 2026.8.4 is out (28d33de033e97a304f77238d6a5db26039ca15d6). Not bumped on purpose: no CVE, and the diff touches version-increment.sh + shared.sh, the calver logic that decides every release tag in push.yaml and branch-preview.yaml. Changing release versioning inside an SCA PR is the wrong blast radius. Worth its own PR with a dry-run.
  • simple-container-com/actions refs are inconsistent: 8 pins at 0af5a697… and semgrep.yml at 5061c0a2…, both commented # main. Not a vulnerability, but the drift is worth a sweep.

Evidence

Source scanners, this branch
$ trivy fs --severity CRITICAL,HIGH,MEDIUM,LOW .
main:   HIGH   github.com/go-git/go-git/v5 v5.19.1 -> 5.19.2  CVE-2026-71556
        MEDIUM github.com/go-git/go-git/v5 v5.19.1 -> 5.19.2  CVE-2026-71557
branch: 0 findings

$ govulncheck -mode=source ./...
0 reachable; 1 module-level (GO-2026-5932, VEX not_affected)

$ osv-scanner scan source -L go.mod
main:   2 vulnerabilities (1 HIGH, 1 MEDIUM), 2 fixable
branch: No issues found   (GO-2026-5932 filtered, justified)

$ pip-audit -r docs/requirements.txt
No known vulnerabilities found
Base-image layers, old digest vs new
alpine:3.23 (old)   C=0 H=2  M=8  L=20  total=30
alpine:3.24 (new)   C=0 H=0  M=0  L=0   total=0
caddy:2.11.4 (old)  C=0 H=12 M=23 L=20  total=55
caddy:2.11.4 (new)  C=0 H=10 M=15 L=0   total=25
alpine/kubectl old  C=0 H=20 M=28 L=20  total=68
alpine/kubectl new  C=0 H=5  M=3  L=0   total=8
lambda al2023 old   C=0 H=58 M=5  L=1   total=64
lambda al2023 new   C=0 H=0  M=0  L=0   total=0
                                        217 -> 33
python base image comparison (welder build-docs)
python:3.9.18     C=248 H=2293 M=5849 L=1720   total=10110   (EOL Oct 2025)
python:3.12       C=60  H=334  M=687  L=1178   total=2259
python:3.12-slim  C=4   H=23   M=86   L=99     total=212     <- chosen
python:3.13-slim  C=4   H=19   M=56   L=66     total=145

3.12-slim over 3.13-slim on purpose: docs/requirements.txt is pip-compiled for Python 3.12 with --generate-hashes, so 3.12 is the version the lockfile was resolved against. The 67-finding gap is all LOW/MEDIUM distro noise. Build verified end to end against the real lockfile:

$ docker run --rm -v "$PWD/docs:/w" -w /w python:3.12-slim sh -c \
    'pip install -r requirements.txt && mkdocs build'
INFO - Documentation built in 2.86 seconds

(The pre-existing ai-assistant/examples/* broken-link warnings are unchanged and unrelated.) mkdocs.yml uses only search + mermaid2, no git-dependent plugins, so -slim is sufficient.

Test plan

  • go mod tidy clean; go build ./cmd/github-actions ./cmd/cloud-helpers succeeds. Our own dist/github-actions links golang.org/x/text v0.40.0 (patched) per go version -m.
  • All 5 images build from a clean context on both origin/main and this branch; the before/after tables above come from those builds, not from published tags.
  • caddy image smoke-verified post-bump: v2.11.4, alpine 3.23.5, both caddy.storage.gcs and http.handlers.rate_limit registered, so the Dockerfile's build-time module assertion passes.
  • github-actions image build-time smoke test (pulumi version, gcloud version, gke-gcloud-auth-plugin present, aws --version, sc symlink) passes on alpine 3.24 + gcloud 579.0.0.
  • docs build verified in python:3.12-slim against the real hashed lockfile.
  • Action SHAs verified against the GitHub git-ref API with annotated tags dereferenced; no pin was hand-written.

Round 2: multi-model review + preview build

Six independent reviewers (four Claude lenses, OpenAI Codex, Google Gemini). Every claim below was re-verified before acting on it; two findings were defects in round 1's own diff, and one review claim turned out to be a probe artifact.

Verified in a real pipeline

branch-preview.yaml dispatched twice, once per commit. Both 13/13 jobs green, including the docker builds — the second run with fully cold caches, since every scope name changed.

round 1 (35cde9a) round 2 (64ddbee)
preview version v2026.8.4-pre.35cde9a-preview.35cde9a v2026.8.4-pre.64ddbee-preview.64ddbee
github-actions image C0 H1 M0 — total 1 C0 H1 M0 — total 1
cloud-helpers image C0 H0 M0 — total 0

The CI-built image reports alpine 3.24.1, Google Cloud SDK 579.0.0, pulumi v3.245.0, gke-gcloud-auth-plugin present. Against the published :latest from main (128 findings, 2 CRITICAL) that is 128 → 1, and the one residual is the documented x/text v0.38.0 inside Google's auth plugin. The gcloud SHA256 pin verified inside a real build, which is the authoritative check on that value.

Two defects in round 1's own diff

1. The cache scoping was wrong, not merely inconsistent. Round 1 scoped per image — but github-actions is a matrix value in both push.yaml (matrix.image) and branch-preview.yaml (matrix.target), so they collided on one scope. branch-preview is dispatchable from any branch and mode=max exports builder stages, so a preview build could seed the very layer a release build restores — and a cache hit means the SHA-256-verified gcloud and Pulumi downloads never execute. Round 1 separated staging but not preview, which made the gap easy to miss.

Scopes are now keyed by trust tier: releases read and write prod-* only; staging and preview write their own tier and read prod-* as a warm fallback, which is the safe direction (reading higher-trust cannot poison it). branch-preview gained an explicit cache_scope so its fallback matches push.yaml's image naming (cloud-helpers-aws, not cloud-helpers) — Gemini and two Claude lenses independently flagged that mismatch. Expect one cold release build after merge; that is the scope rename, not a regression.

2. welder.yaml weakened a pin. Round 1 replaced python:3.9.18 with a floating python:3.12-slim and a bare pip install, against this repo's own documented digest-pin + --require-hashes policy — while push.yaml:376 already pinned the same tool by digest. Now digest-pinned and hash-enforced, identical to the CI docs build, and both refreshed to the current digest (round 1 left push.yaml's three months stale).

Pre-existing defects the panel surfaced

The Pulumi integrity gate could not fail the build. ... && sha256sum -c - && ... && strip … || true && upx … || true parses as ((chain || true) && upx) || true: && and || are equal precedence and left-associative, and errexit does not apply to non-final commands in an AND-OR list. Verified both ways — with a deliberately wrong expected hash, the old shape exits 0, the brace-grouped shape now fails:

#8 1.650 sha256sum: WARNING: 1 of 1 computed checksums did NOT match
#8 ERROR: process "/bin/sh -c set -euo pipefail ..." did not complete successfully: exit code: 1

The gcloud chain terminates on a real command and was already fail-closed, so the gcloud claim in this PR was accurate. Fixed in both github-actions Dockerfiles.

simplecontainer/cloud-helpers:aws-latest cannot start. actions/upload-artifact does not preserve the executable bit, and push.yaml has no chmod for this binary — branch-preview.yaml:324 does. The published image has /cloud-helpers at -rw-r--r-- with ENTRYPOINT ["/cloud-helpers"]. Asserted in the Dockerfile rather than a workflow so it holds whichever path builds it; the CI-built preview image now reports -rwxr-xr-x.

Docs example images carried the CVEs this PR claims to close. docs/DEPENDENCIES.md lists example dirs in the tracked Docker surface, but dependabot.yml scanned only the root level and rationalised the gap in a comment. blog-platform/caddy.Dockerfile pinned caddy 2.11.3 — the exact version whose CVEs this PR fixes at the root — and six files pinned node 22.22.3 (CVE-2026-48618, CVE-2026-48933, both HIGH; fixed in 22.23.0). All bumped to digests re-resolved against the live registry, the docker ecosystem now globs docs/docs/examples/**, and one example moves off EOL python:3.9.

Caddy: version now single-sourced from the base image

Rather than keep three 2.11.x literals guarded by a comment, xcaddy now builds "${CADDY_VERSION}" — the env var the caddy base images set themselves. There is no second literal to forget, which is precisely what shipped a 2.11.3 binary on a 2.11.4 base. Both stages assert the built binary against their own base's version, and certmagic-gcs joins rate_limit in the module assertions (a silently dropped storage plugin means per-pod ACME state and Let's Encrypt lockout).

Worth recording how this landed: my first attempt compared the binary to a hand-maintained ARG, and the negative test showed it passing on a deliberate skew — self-consistent by construction, proving nothing. My second negative test was also invalid, because the digest I picked for "2.11.3" was actually the old 2.11.4 rebuild. Only the third attempt, against the genuine caddy:2.11.3 digest, demonstrated the gate firing:

ERROR: process "/bin/sh -c test -n \"${CADDY_VERSION}\" && caddy version | grep -qF \"${CADDY_VERSION} \"" did not complete successfully: exit code: 1

VEX corrections

The GO-2026-5932 statement claimed x/crypto was "a direct dependency for chacha20poly1305 only" while stamped re-confirmed against the current tree. It is not: 24 x/crypto packages link, including ssh, ssh/agent, ssh/knownhosts, pkcs12 and ocsp, and pkg/api/secrets imports ssh directly. The conclusion is unchanged — openpgp linkage is 0 — but understating the linked surface on the one artifact an auditor reads is worse than not writing it, and x/crypto/ssh is the subpackage with the CVE history. Two reviewers caught this independently.

The evidence commands now specify -buildvcs=false: a plain go list -deps can exit non-zero on a VCS error and print nothing, which greps identically to a clean result — the same masking failure mode as || true.

On the aws-sdk-go statements, one reviewer argued component_not_present contradicts an impact_statement that mentions go list -m all, and proposed vulnerable_code_not_present. I kept component_not_present and made the scope explicit instead: for the shipped artifact the component genuinely is absent — 0 packages linked, 0 go.sum entries, 0 in the built binary's BuildInfo — and the residual go list -m all edge is a requirement-graph reference from a transitive go.mod. vulnerable_code_not_present would wrongly imply the component ships and only the vulnerable function is absent.

Doc contracts this pass contradicted

DEPENDENCIES.md stated that every not_affected is mirrored into osv-scanner.toml, and that a superseded one gets flipped to status: fixed — the rule that produced the bug round 1 reverted. Both are now qualified: the mirror covers only advisories osv-scanner actually reports (an unused ignore is itself a finding), and an advisory with introduced: 0 and no fix event can never reach fixed.

One review claim that did not survive

Codex reported a different gcloud SHA-256 than the pin. That was its own probe artifact: it fetched with curl -C - (resume) against a leftover file, producing 97,127,837 bytes and gzip: trailing garbage ignored. A clean download is 96,066,973 bytes hashing to a9a7fbe5…, matching the pin — and CI's own verification passed inside the preview build. No change needed. Codex otherwise reported clean.

Deliberately deferred

These are real but wrong to fold into a dependency PR:

  • go-git 5.19.2 hardens worktree paths. One lens demonstrated that AddFileToGit now rejects a path whose leading component is a symlink (pkg/provisioner/common.go passes .sc/cfg.yaml.template), where 5.19.1 accepted it — and that the test suite passes on both versions, so nothing would catch it. The repo has zero os.Symlink usage in tests. Reverting would reintroduce the CVEs, so the fix is a test plus an actionable error message, and it needs a decision on whether a symlinked .sc is a supported layout. Worth its own issue.
  • reecetech/version-increment at 2024.10.1 (latest 2026.8.4, SHA 28d33de0). Its diff touches the calver logic that names every release tag.
  • github-actions-staging.Dockerfile duplicates ~95 lines of its sibling under a "keep in sync" comment; this PR had to edit the alpine digest and both gcloud ARGs twice.
  • A pin-consistency CI gate (action SHA vs its # vX.Y.Z comment, gcloud version/SHA parity across the two Dockerfiles). Round 1 found a # v4.3.0 comment on a v5.0.5 SHA by hand; a 20-line gh api check would have caught it. # v4 on the codeql pins is corrected to # v4.37.6 here.
  • DEPENDENCIES.md claims trivy image runs per published image — no trivy/grype step exists in any workflow; the shared scan is source-tree only. That is why the stale caddy binary went unnoticed for months.
  • No PR-time build of any Dockerfile; the first build happens post-merge on push.yaml. A docker build --load job on a path filter would close it.
  • certmagic-gcs v0.1.7→v0.1.8, golangci-lint v1→v2, pulumi SDK 11 minors, mermaid 10.6.1 in the docs site.

…nd CI pins

Consolidates the 7 open Dependabot PRs and adds what they could not see.

Source (Go):
  go-git/v5 5.19.1 -> 5.19.2, closing CVE-2026-71556 (HIGH, worktree ops
  follow symlinks) and CVE-2026-71557 (MEDIUM, malicious ref names write
  outside ref storage). Only source-side finding across trivy fs,
  govulncheck -mode=source, osv-scanner and pip-audit.

Container images:
  alpine 3.23 -> 3.24, and refreshed digests for caddy:2.11.4(-builder),
  alpine/kubectl:latest and lambda/provided:al2023. gcloud 567.0.0 ->
  579.0.0 with a recomputed SHA-256.

  caddy.Dockerfile built its binary with `xcaddy build "v2.11.3"` while both
  FROM lines were already on 2.11.4 — the third "2.11.x" site the file header
  warns about was missed. The shipped binary therefore carried CVE-2026-52844
  and CVE-2026-52845, both fixed in 2.11.4.

Release pipeline:
  Every docker/build-push-action step shared one unscoped `type=gha` cache
  namespace, including a 5-way image matrix all writing `mode=max`
  concurrently. simplecontainer/caddy:latest (== :2026.8.3, pushed today)
  ships alpine 3.20.4 with caddy v2.8.4 from an Apr 27 binary and no
  http.handlers.rate_limit module, so it cannot be the output of the current
  Dockerfile whose build-time module assertion would have failed. Cache scope
  is now per image, so base-image bumps actually reach published artifacts.

Docs build:
  welder build-docs ran on python:3.9.18, EOL since Oct 2025 and inconsistent
  with the requirements.txt lockfile compiled for 3.12. Now python:3.12-slim
  (10110 -> 213 image vulns); docs build verified against the real lockfile.

CI pins:
  actions/checkout v6.0.3 -> v7.0.1, actions/cache(+/save) v5.0.5 -> v6.1.0,
  actions/setup-go v6 -> v7.0.0, attest-build-provenance v4.1.0 -> v4.2.2,
  setup-buildx-action v4.1.0 -> v4.2.0, build-push-action v7.2.0 -> v7.3.0,
  scorecard-action v2.4.3 -> v2.4.4, codeql-action v4 SHA refresh. All are
  already node24, so the ESM majors change no runner requirement. One
  actions/cache pin carried a `# v4.3.0` comment on a v5.0.5 SHA; corrected.

VEX re-triage:
  GO-2022-0635 / GO-2022-0646 were marked `status: fixed`, but both carry
  `introduced: 0` with no fix event in OSV, so `fixed` was never reachable
  and was acting as an indefinite suppression. Corrected to `not_affected` /
  `component_not_present` with the real evidence: aws-sdk-go v1.55.8 is in
  the module requirement graph but zero of its packages link into any
  binary. GO-2026-5932 re-confirmed against the current tree.

Signed-off-by: Dmitrii Creed <creeed22@gmail.com>
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown

Semgrep Scan Results

Repository: api | Commit: ee94a13

Check Status Details
⚠️ Semgrep Warning 1 warning(s), 5 total

Scanned at 2026-08-10 18:08 UTC

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown

Security Scan Results

Repository: api | Commit: ee94a13

Check Status Details
✅ Secret Scan Pass No secrets detected
✅ Dependencies (Trivy) Pass 1 total (no critical/high)
✅ Dependencies (Grype) Pass 1 total (no critical/high)
📦 SBOM Generated 523 components (CycloneDX)

Scanned at 2026-08-10 18:08 UTC

@github-actions

Copy link
Copy Markdown

📊 Statement coverage

Measured on the documented included set (see docs/TESTING.md → Coverage scope). Observe-only — no regression gate is enforced yet.

Scope This PR main baseline Δ
Included set (Gold-tier denominator) 90.3% 90.3% +0.0 pp
Full set (whole repo, transparency) 28.4% 28.4% +0.0 pp

Baseline: main @ b2bbb2f

…tes, docs images

Round 2 of the SCA pass, from a six-reviewer panel (4 Claude lenses, Codex,
Gemini). Two findings were defects in round 1's own diff.

Cache scoping was wrong, not just inconsistent:
  Round 1 scoped the buildx GHA cache per image, but `github-actions` is a
  matrix value in BOTH push.yaml (`matrix.image`) and branch-preview.yaml
  (`matrix.target`) — so the two collided on one scope. branch-preview is
  dispatchable from any branch, and `mode=max` exports builder stages, so a
  preview build could seed the layer a release build restores, skipping the
  SHA-256-verified gcloud and Pulumi downloads entirely. Scopes are now keyed
  by trust tier: releases read/write `prod-*` only; staging and preview write
  their own tier and read `prod-*` as a warm fallback, which is the safe
  direction. branch-preview gets an explicit `cache_scope` so its fallback
  matches push.yaml's image names (`cloud-helpers-aws`, not `cloud-helpers`).

The Pulumi integrity gate could not fail:
  `... && sha256sum -c - && ... && strip … || true && upx … || true` parses as
  `((chain || true) && upx) || true`. `&&`/`||` are equal precedence and
  left-associative, and errexit does not apply to non-final commands in an
  AND-OR list, so a checksum mismatch exited 0. Verified before and after:
  a wrong expected hash now fails the build. The gcloud chain ends on a real
  command and was already fail-closed. Brace-grouped in both Dockerfiles.

Caddy version is now single-sourced from the base image:
  `$CADDY_VERSION` is set by the caddy base images themselves, so
  `xcaddy build "${CADDY_VERSION}"` cannot skew from the image it builds in —
  there is no second literal to forget, which is what shipped a 2.11.3 binary
  on a 2.11.4 base. Both stages assert the built binary against their own
  base's version, and certmagic-gcs joins rate_limit in the module assertions
  (a silently dropped storage plugin means per-pod ACME state and Let's
  Encrypt lockout). Negative-tested: a genuine builder/runtime digest skew
  now fails the build.

cloud-helpers shipped a non-executable entrypoint:
  actions/upload-artifact does not preserve the exec bit and push.yaml has no
  chmod for this binary (branch-preview.yaml does). The published
  simplecontainer/cloud-helpers:aws-latest has /cloud-helpers at 0644 with
  `ENTRYPOINT ["/cloud-helpers"]`, so it cannot start. Asserted in the
  Dockerfile so it holds whichever workflow builds it.

Docs example images were carrying the CVEs this pass claims to close:
  docs/DEPENDENCIES.md lists example dirs in the tracked Docker surface, but
  dependabot.yml only scanned the root level and rationalised the gap in a
  comment. blog-platform pinned caddy 2.11.3 (CVE-2026-52844/52845 HIGH) and
  six files pinned node 22.22.3 (CVE-2026-48618, CVE-2026-48933 HIGH). Both
  bumped, the docker ecosystem now globs docs/docs/examples/**, and the
  advanced-configs example moves off EOL python:3.9.

welder.yaml docs build no longer weakens the pin:
  round 1 replaced python:3.9.18 with a floating python:3.12-slim and a bare
  pip install, against the repo's own digest-pin and --require-hashes policy.
  Now digest-pinned and hash-enforced, identical to push.yaml's docs build —
  and both refreshed to the current digest, which round 1 missed.

VEX accuracy:
  the GO-2026-5932 statement claimed x/crypto was "a direct dependency for
  chacha20poly1305 only" while stamped re-confirmed. It is not: 24 x/crypto
  packages link, including ssh, ssh/agent, pkcs12 and ocsp. The conclusion is
  unchanged (openpgp linkage is 0) but the evidence now matches the tree, and
  cites -buildvcs=false because a plain `go list` can exit non-zero and print
  nothing, which greps identically to a clean result. The aws-sdk-go
  statements now say which scope `component_not_present` refers to.

Docs contracts that this pass contradicted:
  DEPENDENCIES.md said every not_affected is mirrored into osv-scanner.toml
  and that a superseded one gets flipped to `fixed`. Both now qualified: the
  mirror covers only advisories osv-scanner actually reports, and an advisory
  with `introduced: 0` and no fix event can never reach `fixed`.

Also: codeql/scorecard pins carried `# v4` where every neighbour has full
semver; the SHA is v4.37.6.

Signed-off-by: Dmitrii Creed <creeed22@gmail.com>
@Cre-eD
Cre-eD merged commit bd30332 into main Aug 11, 2026
37 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants