diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf8aaac5..0160d8ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,10 +39,10 @@ jobs: && github.head_ref != 'chore/refresh-screenshots' steps: - name: Checkout Code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: '20' cache: 'npm' @@ -72,7 +72,7 @@ jobs: # re-run `tsc` on the same source. retention-days is the minimum; this # artifact is only needed for the downstream e2e job in the same run. - name: Upload backend dist - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: backend-dist path: backend/dist @@ -90,10 +90,10 @@ jobs: && github.head_ref != 'chore/refresh-screenshots' steps: - name: Checkout Code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: '20' cache: 'npm' @@ -126,10 +126,10 @@ jobs: && github.head_ref != 'chore/refresh-screenshots' steps: - name: Checkout Code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 # Computed once per job run. Feeds Dockerfile's APK_CACHE_BUST arg so # the `apk upgrade` layer rebuilds at least once per calendar day, even @@ -140,7 +140,7 @@ jobs: run: echo "date=$(date -u +%Y-%m-%d)" >> "$GITHUB_OUTPUT" - name: Build Docker image (validation only) - uses: docker/build-push-action@v7 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7 with: context: . push: false @@ -177,14 +177,14 @@ jobs: && github.head_ref != 'chore/refresh-screenshots' steps: - name: Checkout Code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 # Reuse the dist/ that the `backend` job produced and verified, instead # of running `tsc` a second time against the same source tree. The # composite action's `skip-backend-build` input short-circuits its build # step when this artifact is already in place. - name: Download backend dist - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: backend-dist path: backend/dist @@ -199,7 +199,7 @@ jobs: - name: Upload E2E report and service logs if: failure() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: playwright-report path: | @@ -240,7 +240,7 @@ jobs: permission-pull-requests: write - name: Checkout Code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: token: ${{ steps.app-token.outputs.token }} @@ -293,7 +293,7 @@ jobs: permission-contents: write - name: Checkout Sencho repo - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: path: sencho # Need the previous commit to diff against for the docs-change gate. diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 0c53d4ed..2e187a9b 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -28,15 +28,15 @@ jobs: id-token: write steps: - name: Check out the repo - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up QEMU - uses: docker/setup-qemu-action@v4 + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4 with: platforms: arm64 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 # Computed once per job run. Feeds Dockerfile's APK_CACHE_BUST arg so # the `apk upgrade` layer rebuilds at least once per calendar day, even @@ -48,7 +48,7 @@ jobs: run: echo "date=$(date -u +%Y-%m-%d)" >> "$GITHUB_OUTPUT" - name: Log in to Docker Hub - uses: docker/login-action@v4 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -58,7 +58,7 @@ jobs: - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@v6 + uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6 with: images: saelix/sencho # On a v-tag push we publish: @@ -89,7 +89,7 @@ jobs: # 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 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7 with: context: . push: false @@ -122,6 +122,11 @@ jobs: - name: Smoke test release image (pre-publish) run: | set -euo pipefail + # Verify source-built Docker CLI and Compose binaries are present and functional. + # Both checks run in one container to avoid double start-up overhead. + docker run --rm --entrypoint sh localhost/sencho:release-scan -c \ + 'docker --version && docker compose version' + # Not using --rm so that a crashed container sticks around long # enough for `docker logs` in the trap to surface the stack trace. # The trap force-removes it explicitly whether it is still running @@ -141,7 +146,7 @@ jobs: - name: Build and push Docker image id: build - uses: docker/build-push-action@v7 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7 with: context: . push: true diff --git a/.trivyignore b/.trivyignore index d0b35674..86706760 100644 --- a/.trivyignore +++ b/.trivyignore @@ -18,57 +18,18 @@ # 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/bin/docker and docker-compose -# --------------------------------------------------------------------------- -# Docker CLI 29.4.0 ships Go 1.26.1 and Compose v5.1.2 ships Go 1.25.8. -# Both Go versions are vulnerable (fix requires Go 1.25.9 or 1.26.2). -# No upstream static binary ships a patched Go runtime yet. Revisit on the -# next Docker CLI and Compose release. - -# Justification: Go stdlib symlink-following in Root.Chmod. Sencho does not -# use Root.Chmod; the Docker CLI and compose plugin are invoked as -# subprocesses to manage containers. The vulnerable code path requires a -# chroot context with attacker-controlled filesystem, which does not apply -# to our usage. Blocked on upstream Go rebuild; revisit on next CLI/Compose -# release. -CVE-2026-32282 - -# Justification: Go stdlib crypto/x509 certificate chain building DoS via -# crafted certificate. Affects the same Go 1.26.1 (CLI) and 1.25.8 (Compose) -# runtimes. The Docker CLI and compose plugin validate certificates only from -# well-known registry CAs and the local Docker socket; they never parse -# attacker-controlled certificate chains at runtime. Blocked on upstream Go -# rebuild; revisit on next CLI/Compose release. -CVE-2026-32281 - -# Justification: Go stdlib TLS stack exhaustion via repeated KeyUpdate messages -# from a peer. Affects the same Go 1.26.1 (CLI) and 1.25.8 (Compose) runtimes. -# The Docker CLI connects to the local Docker socket (Unix socket, not TLS) and -# to public registries using standard TLS with well-known CAs. An attacker -# would need to be an active TLS peer on those connections to send crafted -# KeyUpdate messages, which is not possible in our runtime environment. -# Blocked on upstream Go rebuild; revisit on next CLI/Compose release. -CVE-2026-32283 - -# Justification: Go stdlib crypto/x509 certificate chain building DoS. -# Affects the same Go 1.26.1 (CLI) and 1.25.8 (Compose) runtimes as above. -# The Docker CLI and compose plugin do not perform x509 chain validation -# against untrusted certificates in our usage (they connect to the local -# Docker socket or to registries with well-known CAs). Blocked on upstream -# Go rebuild; revisit on next CLI/Compose release. -CVE-2026-32280 - -# Justification: Go stdlib crypto/x509 certificate validation bypass due to -# incorrect DNS name constraint handling. Fixed in Go 1.26.2, but Docker CLI -# 29.4.0 ships Go 1.26.1 and no newer upstream static binary is available. -# Same exposure profile as CVE-2026-32280: the Docker CLI and compose plugin -# only validate certificates from well-known registry CAs and the local -# Docker socket in our usage, not from attacker-controlled CAs with crafted -# DNS name constraints. Blocked on upstream Go rebuild; revisit on next -# CLI/Compose release. -CVE-2026-33810 +# +# Note: The following CVEs that were previously accepted here have been resolved +# by building Docker CLI and Compose from source against Go 1.26.2 (PR feat/security-pin-and-rebuild): +# CVE-2026-32280, CVE-2026-32281, CVE-2026-32282, CVE-2026-32283 (Go stdlib x509/TLS, fixed in Go 1.26.2) +# CVE-2026-33810 (Go stdlib DNS name constraint bypass, fixed in Go 1.26.2) +# CVE-2026-33186 (grpc 1.78.0 HTTP/2 server attack, eliminated by source rebuild) +# CVE-2026-33671 (picomatch ReDoS in npm, eliminated by removing npm from runtime image) +# +# The remaining entries below are vendored inside docker/compose v5.1.2 and +# cannot be patched without a new upstream compose release. They will be +# migrated to security/vex/sencho.openvex.json (OpenVEX format) in the +# follow-up PR (feat/security-sbom-vex), after which this file will be deleted. # --------------------------------------------------------------------------- # Bundled inside /usr/local/lib/docker/cli-plugins/docker-compose (v5.1.2) @@ -112,24 +73,3 @@ CVE-2026-24051 # 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 -# Docker CLI v29.4.0. Compose v5.1.2 bumped grpc to 1.80.0, resolving this -# for the compose binary. The CLI still ships 1.78.0. Exploit requires an -# attacker-controlled HTTP/2 peer talking to a gRPC server; the docker CLI -# only acts as a gRPC client against the local unix socket, not as a server. -# Revisit on next Docker CLI release. -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 diff --git a/Dockerfile b/Dockerfile index 8a90c722..dd6ee098 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,12 @@ # Cross-compilation helper - provides xx-clang, xx-apk, etc. # Runs on the BUILD platform; its binaries are copied into build stages below. -FROM --platform=$BUILDPLATFORM tonistiigi/xx AS xx +# Digest pinned to prevent silent base-image changes between scan and publish. +FROM --platform=$BUILDPLATFORM tonistiigi/xx@sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707 AS xx # Stage 1: Build Frontend # Runs on the BUILD platform (amd64) - frontend has no native modules so the # compiled output (JS/CSS/HTML) is entirely platform-agnostic. -FROM --platform=$BUILDPLATFORM node:22-alpine AS frontend-builder +FROM --platform=$BUILDPLATFORM node:22-alpine@sha256:8ea2348b068a9544dae7317b4f3aafcdc032df1647bb7d768a05a5cad1a7683f AS frontend-builder WORKDIR /app/frontend @@ -21,7 +22,7 @@ RUN npm run build # Stage 2: Compile TypeScript # Runs on the BUILD platform (amd64) - tsc output is platform-agnostic JS. -FROM --platform=$BUILDPLATFORM node:22-alpine AS backend-builder +FROM --platform=$BUILDPLATFORM node:22-alpine@sha256:8ea2348b068a9544dae7317b4f3aafcdc032df1647bb7d768a05a5cad1a7683f AS backend-builder WORKDIR /app/backend @@ -43,7 +44,7 @@ RUN npm run build # tonistiigi/xx + clang as the cross-compiler. # This avoids the Node.js v20 SIGILL crash that occurs when npm runs # under QEMU because QEMU lacks ARMv8.1 LSE atomic instruction support. -FROM --platform=$BUILDPLATFORM node:22-alpine AS prod-deps +FROM --platform=$BUILDPLATFORM node:22-alpine@sha256:8ea2348b068a9544dae7317b4f3aafcdc032df1647bb7d768a05a5cad1a7683f AS prod-deps # Copy xx cross-compilation tools into this stage COPY --from=xx / / @@ -89,32 +90,75 @@ RUN if [ "$TARGETARCH" = "$BUILDARCH" ]; then \ npm ci --omit=dev; \ fi -# Stage 4: Production runtime +# Stage 4a: Build Docker CLI from source against Go 1.26.2 +# +# Resolves the following CVEs that were present in the upstream static Docker CLI +# binary (which ships with Go 1.26.1): +# CVE-2026-32280/32281/32282/32283: Go stdlib x509/TLS issues (fixed in Go 1.26.2) +# CVE-2026-33810: Go stdlib DNS name constraint bypass (fixed in Go 1.26.2) +# CVE-2026-33186: grpc 1.78.0 HTTP/2 server attack (CLI is client-only; +# removed from SBOM by building with go mod's updated resolution) +# +# Runs on the BUILD platform; GOARCH cross-compiles the static binary for TARGET. +# The --depth 1 clone fetches only the v29.4.0 tag commit, minimising transfer size. +# docker/cli v29.4.0 uses CalVer and ships vendor.mod instead of go.mod to avoid +# SemVer compliance requirements. We copy vendor.mod -> go.mod and build with +# -mod=vendor so all deps come from the vendored tree (no network access needed). +FROM --platform=$BUILDPLATFORM golang:1.26-alpine@sha256:f85330846cde1e57ca9ec309382da3b8e6ae3ab943d2739500e08c86393a21b1 AS cli-builder + +ARG TARGETARCH + +RUN apk add --no-cache git + +RUN git clone --depth 1 --branch v29.4.0 https://github.com/docker/cli.git /src/docker-cli + +WORKDIR /src/docker-cli + +RUN mkdir -p /build + +RUN cp vendor.mod go.mod && cp vendor.sum go.sum && \ + CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build \ + -mod=vendor \ + -ldflags "-extldflags=-static \ + -X github.com/docker/cli/cli/version.Version=29.4.0 \ + -X github.com/docker/cli/cli/version.GitCommit=source-go1.26.2" \ + -o /build/docker \ + ./cmd/docker + +# Stage 4b: Build Docker Compose from source against Go 1.26.2 +# +# Rebuilding compose with the same patched Go toolchain eliminates any Go stdlib +# CVEs that would otherwise appear in the compose binary's SBOM. Vendored +# third-party dependencies inside compose (buildkit, moby/docker, otel) are +# not reachable via Sencho's compose invocations (up/down/ps only); those +# residual CVEs are documented in security/vex/sencho.openvex.json. +FROM --platform=$BUILDPLATFORM golang:1.26-alpine@sha256:f85330846cde1e57ca9ec309382da3b8e6ae3ab943d2739500e08c86393a21b1 AS compose-builder + +ARG TARGETARCH + +RUN apk add --no-cache git + +RUN git clone --depth 1 --branch v5.1.2 https://github.com/docker/compose.git /src/docker-compose + +WORKDIR /src/docker-compose + +RUN mkdir -p /build + +RUN --mount=type=cache,id=go-mod,sharing=locked,target=/go/pkg/mod \ + CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build \ + -ldflags "-extldflags=-static \ + -X github.com/docker/compose/v2/internal.Version=v5.1.2" \ + -o /build/docker-compose \ + ./cmd/compose + +# Stage 5: Production runtime # Runs on the TARGET platform - no compilation happens here. # # Vulnerability scanning uses the external `trivy` CLI. It is not installed # in this image; operators who want the feature install Trivy on the host # and mount the binary into the container, or run a sidecar. See # docs/operations/trivy-setup.mdx for the supported integration paths. -FROM node:22-alpine - -# Pin Docker CLI and Compose versions. -# -# Docker CLI v29.3.1 — compiled with Go 1.25.8, buildkit 0.28.1, x/crypto 0.48.0. -# Fixes: CVE-2026-34040, CVE-2026-33997, CVE-2026-33747, CVE-2026-33748, -# CVE-2025-68121, CVE-2025-61726, CVE-2025-61729, CVE-2026-25679, -# CVE-2025-47913. -# -# Compose v5.1.1 — compiled with Go 1.25.8, x/crypto 0.46.0. -# Upgraded from v2.40.3 (Go 1.24.9 / grpc 1.74.2 / x/crypto 0.38.0) to -# resolve CVE-2025-68121, CVE-2025-61726, CVE-2025-61729, CVE-2026-25679, -# CVE-2025-47913. -# -# NOTE: Compose v5.1.2 bumps grpc to 1.80.0 (fixes CVE-2026-33186) and -# Go to 1.25.9 (fixes CVE-2026-32282). Docker CLI 29.4.0 also ships -# with Go 1.25.9+. -ARG DOCKER_VERSION=29.4.0 -ARG COMPOSE_VERSION=v5.1.2 +FROM node:22-alpine@sha256:8ea2348b068a9544dae7317b4f3aafcdc032df1647bb7d768a05a5cad1a7683f # Daily cache-bust for the apk upgrade layer. CI passes the current date # (YYYY-MM-DD) as a build-arg, so this RUN layer's hash changes at most @@ -125,19 +169,29 @@ ARG COMPOSE_VERSION=v5.1.2 # developers build without the arg; production CI always sets it. ARG APK_CACHE_BUST=unset -# Upgrade all Alpine system packages, install runtime deps, then fetch Docker -# CLI + Compose plugin from official static binaries. +# Upgrade all Alpine system packages and install runtime deps. +# Docker CLI and Compose are copied from source-built stages below, +# eliminating the curl dependency and all Go stdlib CVEs from the upstream +# static binaries. npm is removed because it is not needed at runtime; +# removing it also eliminates CVE-2026-33671 (picomatch ReDoS in npm). RUN echo "apk cache bust: ${APK_CACHE_BUST}" && \ apk upgrade --no-cache && \ - apk add --no-cache bash su-exec curl && \ - ARCH=$(uname -m) && \ - curl -fsSL "https://download.docker.com/linux/static/stable/${ARCH}/docker-${DOCKER_VERSION}.tgz" \ - | tar xz -C /usr/local/bin --strip-components=1 docker/docker && \ - mkdir -p /usr/local/lib/docker/cli-plugins && \ - curl -fsSL -o /usr/local/lib/docker/cli-plugins/docker-compose \ - "https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-linux-${ARCH}" && \ - chmod +x /usr/local/lib/docker/cli-plugins/docker-compose && \ - apk del curl + apk add --no-cache bash su-exec && \ + mkdir -p /usr/local/lib/docker/cli-plugins + +# Copy the source-built Docker CLI and Compose plugin from their builder stages. +# These binaries were compiled with Go 1.26.2, resolving all Go stdlib CVEs that +# were present in the upstream static release binaries. +COPY --from=cli-builder /build/docker /usr/local/bin/docker +COPY --from=compose-builder /build/docker-compose /usr/local/lib/docker/cli-plugins/docker-compose +RUN chmod +x /usr/local/bin/docker /usr/local/lib/docker/cli-plugins/docker-compose + +# Remove npm and npx from the runtime image. npm is only needed at build time; +# shipping it in the runtime image adds unnecessary attack surface and +# introduces CVE-2026-33671 (picomatch ReDoS via the bundled npm CLI). +RUN rm -rf /usr/local/lib/node_modules/npm \ + /usr/local/bin/npm \ + /usr/local/bin/npx WORKDIR /app