diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6fa878a..e9f0bf94 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -123,15 +123,15 @@ jobs: cache-to: type=gha,mode=max - name: Scan image for vulnerabilities (Trivy) + # Hard-fails the PR on any HIGH or CRITICAL finding that is not listed + # in .trivyignore at the repo root. To accept a CVE, add it to that + # file with a human-readable justification comment, not here. uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0 with: image-ref: sencho:pr-test exit-code: '1' severity: 'CRITICAL,HIGH' format: 'table' - # Findings surface as a visible warning on the job without blocking the PR. - # Base-image CVEs are often outside our control - review manually before ignoring. - continue-on-error: true # --------------------------------------------------------------------------- # E2E Tests (PRs only, skipped for release-please PRs) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 0cf80c42..b69d3fdb 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -63,6 +63,39 @@ jobs: type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') }} type=semver,pattern={{major}}.{{minor}},enable=${{ startsWith(github.ref, 'refs/tags/v') }} + # Build an amd64-only variant into the local daemon first so Trivy can + # scan the exact release artifact before it is tagged and pushed. This + # keeps vulnerable releases out of the `latest` and semver tags that + # users actually pull. The scan-build inherits layers from the shared + # buildcache so the amd64 leg of the subsequent push-build is mostly a + # cache hit; we intentionally do NOT write `cache-to` here because the + # push-build below writes a strictly better (multi-arch, mode=max) + # cache entry moments later. The tag lives in the `localhost/` namespace + # so a future `push: false` -> `push: true` mistake cannot publish it. + # Scanning only amd64 is a defensible proxy for the multi-arch release: + # distro package CVEs are arch-agnostic at the manifest level, and + # arch-specific container-relevant CVEs are extraordinarily rare. + - name: Build release image for pre-publish scan (amd64, loaded) + uses: docker/build-push-action@v7 + with: + context: . + push: false + load: true + platforms: linux/amd64 + tags: localhost/sencho:release-scan + cache-from: type=registry,ref=saelix/sencho:buildcache + + - name: Re-scan release image for vulnerabilities (Trivy) + # Gates the release on the same HIGH/CRITICAL policy as the PR scan. + # Entries in .trivyignore at the repo root are honored so the acknowledged + # CVE list is the single source of truth across PR CI and release CI. + uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0 + with: + image-ref: localhost/sencho:release-scan + exit-code: '1' + severity: 'CRITICAL,HIGH' + format: 'table' + - name: Build and push Docker image id: build uses: docker/build-push-action@v7 diff --git a/.trivyignore b/.trivyignore new file mode 100644 index 00000000..a8e96f0c --- /dev/null +++ b/.trivyignore @@ -0,0 +1,87 @@ +# Trivy ignore list +# +# Every entry in this file is a known HIGH or CRITICAL CVE that we have +# consciously accepted risk on and decided not to block CI over. Format: +# +# CVE-YYYY-NNNNN +# # Justification: why we're accepting this risk, and a link or note about +# # when to revisit (e.g. "blocked on upstream base image update, revisit +# # when alpine/node:22 ships a fix"). +# +# Rules: +# - Every CVE MUST have a justification comment directly above it. +# - If there is no justification, the CVE is not ignored - add it here only +# after a human review and a decision to accept the risk. +# - Review this file on every release; remove entries whose upstream fix has +# landed. +# +# Picked up automatically by aquasecurity/trivy-action from the repo root +# working directory. Both the pre-push PR scan (.github/workflows/ci.yml) and +# the release-time re-scan (.github/workflows/docker-publish.yml) honor it. + +# --------------------------------------------------------------------------- +# Bundled inside /usr/local/lib/docker/cli-plugins/docker-compose (v5.1.1) +# --------------------------------------------------------------------------- +# Compose v5.1.1 is the latest upstream release and is already pinned in +# Dockerfile:113 to get us off the v2.40.3 grpc 1.74.2 / x/crypto 0.38.0 CVE +# set. It statically links older copies of github.com/docker/docker, +# buildkit, otel, and grpc. We cannot bump these transitively without +# waiting for a new upstream Compose release. Revisit this block on every +# Compose release; remove entries as upstream rebuilds ship the fixes. +# See the rationale block at Dockerfile:96-111. + +# Justification: github.com/docker/docker v28.5.2 statically bundled in +# compose v5.1.1. Moby authz bypass applies to a Docker daemon, not to the +# compose CLI plugin; compose never runs as a daemon. Revisit on next +# Compose upstream release. +CVE-2026-34040 + +# Justification: github.com/moby/buildkit v0.27.1 statically bundled in +# compose v5.1.1. BuildKit arbitrary file write via untrusted frontend is +# exploited at buildkit build time with attacker-controlled frontends; our +# compose invocations only call up/down/ps against local user-authored +# compose files, never as a build frontend. Revisit on next Compose upstream +# release. +CVE-2026-33747 + +# Justification: github.com/moby/buildkit v0.27.1 statically bundled in +# compose v5.1.1. Same exposure profile as CVE-2026-33747 (Git URL fragment +# subdir exploitation requires invoking buildkit on untrusted repo URLs, +# which compose does not do in our flow). Revisit on next Compose upstream +# release. +CVE-2026-33748 + +# Justification: go.opentelemetry.io/otel/sdk v1.38.0 statically bundled in +# compose v5.1.1. PATH hijacking requires the attacker to control the +# process PATH before compose starts; our container starts compose from a +# fixed PATH with only /usr/local/bin and /usr/bin on it, both owned by +# root. Revisit on next Compose upstream release. +CVE-2026-24051 + +# Justification: go.opentelemetry.io/otel/sdk v1.38.0 statically bundled in +# compose v5.1.1. BSD kenv PATH hijacking only applies on BSD systems; we +# ship linux/amd64 and linux/arm64. Not applicable in our runtime. Revisit +# on next Compose upstream release. +CVE-2026-39883 + +# Justification: google.golang.org/grpc v1.78.0 statically bundled in +# compose v5.1.1 AND in Docker CLI v29.3.1. Already explicitly acknowledged +# in the Dockerfile rationale block at Dockerfile:108-111 as unpatched, +# waiting on a new Docker CLI or Compose release that ships grpc >= 1.79.3. +# Exploit requires an attacker-controlled HTTP/2 peer talking to a gRPC +# server; compose and the docker CLI only act as gRPC clients against the +# local unix socket, not as servers. +CVE-2026-33186 + +# --------------------------------------------------------------------------- +# Bundled inside /usr/local/lib/node_modules/npm/ (node:22-alpine base image) +# --------------------------------------------------------------------------- + +# Justification: picomatch 4.0.3 is shipped inside the npm CLI that comes +# bundled with the upstream node:22-alpine base image. We do not run npm at +# container runtime against user-controlled input; npm is only invoked at +# build time against our own package.json files. The ReDoS requires an +# attacker-authored extglob pattern, which is not reachable from any +# runtime code path. Revisit when a future node:22-alpine base image ships +# a newer npm that bundles picomatch >= 4.0.4. +CVE-2026-33671