From 9231f7b2b6b83de2833849bcc351e2560e2d54ca Mon Sep 17 00:00:00 2001 From: SoundMindsAI Date: Tue, 2 Jun 2026 21:29:12 -0400 Subject: [PATCH 1/2] ci(security): add CodeQL SAST + write Dockerfile base-image digests literally MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two follow-ups on the post-merge OSSF Scorecard surface (PR #430 closed the postcss vuln + ~50 pinning alerts; this closes the SAST finding + the 6 remaining containerImage alerts). CodeQL (#71): add .github/workflows/codeql.yml — GitHub first-party SAST on push/PR to main + weekly, scanning python + javascript-typescript with build-mode none. Closes the Scorecard "SAST tool is not run on all commits" finding and surfaces real code-level bugs on every PR. Action refs pinned to SHAs to match the repo's pinning posture. Dockerfile digests (#59/#60/#80-#83): revert the BASE_IMAGE ARG indirection back to literal `image@sha256:…` on each FROM. The pin was always real, but Scorecard's static parser only credits a digest it can see inline — an ARG-indirected digest reads as "unpinned", which is what kept all 6 containerImage alerts open (Dockerfile:24/46/79 + ui/Dockerfile:24/30/39, confirmed from the Scorecard SARIF locations). Writing tag+digest together also removes the override footgun Gemini flagged on PR #430; Dependabot bumps every FROM occurrence in lockstep, so the repeated node digest is not a sync hazard. Both Dockerfiles pass `docker buildx build --check`. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: SoundMindsAI --- .github/workflows/codeql.yml | 63 ++++++++++++++++++++++++++++++++++++ Dockerfile | 15 +++++---- ui/Dockerfile | 17 +++++----- 3 files changed, 80 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..8698a55e --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,63 @@ +# SPDX-FileCopyrightText: 2026 soundminds.ai +# +# SPDX-License-Identifier: Apache-2.0 + +# CodeQL — GitHub's first-party static analysis (SAST). Closes the OSSF +# Scorecard "SAST tool is not run on all commits" finding and, more importantly, +# surfaces real code-level security bugs on every PR. Scans both languages in +# the repo: Python (backend) and JavaScript/TypeScript (ui). Both use +# build-mode `none` — CodeQL extracts interpreted-language source directly, so +# no compile step is needed (and no engine/cluster service containers). +# +# Action refs are pinned to commit SHAs to match the repo's pinned-dependencies +# posture (chore_scorecard_pin_deps_postcss); Dependabot's github-actions +# ecosystem keeps the SHAs fresh. +name: CodeQL + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + # Mondays 04:31 UTC — offset from scorecard.yml's 04:27 to spread load. + - cron: "31 4 * * 1" + +# Least-privilege default; the analyze job widens to security-events: write. +permissions: + contents: read + +concurrency: + group: codeql-${{ github.ref }} + cancel-in-progress: true + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + security-events: write # upload CodeQL SARIF to the Security tab + strategy: + fail-fast: false + matrix: + include: + - language: python + build-mode: none + - language: javascript-typescript + build-mode: none + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Initialize CodeQL + uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4 + with: + category: "/language:${{ matrix.language }}" diff --git a/Dockerfile b/Dockerfile index f6f5a00a..abd6aebf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,16 +12,17 @@ # Per docs/01_architecture/deployment.md §"MVP1 deployment shape" + the # implementation_plan.md Story 4.1 "Decision rationale". -# Single digest-pinned base image (PinnedDependencies / OSSF Scorecard). -# tag + digest in one ARG so an override (`--build-arg BASE_IMAGE=...`) is -# unambiguous — a separate version/digest pair would let the digest silently -# win over a changed tag. Dependabot's docker ecosystem keeps this fresh. -ARG BASE_IMAGE=python:3.14-slim@sha256:c845af9399020c7e562969a13689e929074a10fd057acd1b1fad06a2fb068e97 - # --------------------------------------------------------------------------- # Stage 1 — base: Python + uv + system deps for healthcheck (curl) # --------------------------------------------------------------------------- -FROM ${BASE_IMAGE} AS base +# python:3.14-slim, digest-pinned (PinnedDependencies / OSSF Scorecard). The +# digest is written literally on the FROM (not via an ARG) because Scorecard's +# static parser only credits a pin it can see inline as `image@sha256:…`; an +# ARG-indirected digest reads as "unpinned". Writing the tag + digest together +# also removes the override footgun of a separate version/digest pair (the +# digest would silently win over a changed tag). Dependabot's docker ecosystem +# bumps the tag + digest together; refresh both when bumping Python. +FROM python:3.14-slim@sha256:c845af9399020c7e562969a13689e929074a10fd057acd1b1fad06a2fb068e97 AS base ENV PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 \ diff --git a/ui/Dockerfile b/ui/Dockerfile index 204f7a9a..77d579df 100644 --- a/ui/Dockerfile +++ b/ui/Dockerfile @@ -16,18 +16,19 @@ # Node 26 to use the latest engine features without waiting for the # October LTS transition. -# Single digest-pinned base image (PinnedDependencies / OSSF Scorecard), -# declared once and reused by every stage. Dependabot's docker ecosystem -# keeps the digest fresh. -ARG BASE_IMAGE=node:26-bookworm-slim@sha256:79723b41edbedf595f62e943a9f8b0ba9af5b1e61045c5f8f59c2c02c1212a16 - -FROM ${BASE_IMAGE} AS deps +# node:26-bookworm-slim, digest-pinned (PinnedDependencies / OSSF Scorecard). +# The digest is written literally on every FROM (not via an ARG) because +# Scorecard's static parser only credits a pin it can see inline as +# `image@sha256:…`; an ARG-indirected digest reads as "unpinned". Dependabot's +# docker ecosystem bumps every occurrence in lockstep, so the repetition is not +# a sync hazard. +FROM node:26-bookworm-slim@sha256:79723b41edbedf595f62e943a9f8b0ba9af5b1e61045c5f8f59c2c02c1212a16 AS deps WORKDIR /app RUN npm install -g pnpm@9 COPY pnpm-lock.yaml package.json ./ RUN pnpm install --frozen-lockfile -FROM ${BASE_IMAGE} AS builder +FROM node:26-bookworm-slim@sha256:79723b41edbedf595f62e943a9f8b0ba9af5b1e61045c5f8f59c2c02c1212a16 AS builder WORKDIR /app RUN npm install -g pnpm@9 COPY --from=deps /app/node_modules ./node_modules @@ -36,7 +37,7 @@ ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL RUN pnpm build -FROM ${BASE_IMAGE} AS runner +FROM node:26-bookworm-slim@sha256:79723b41edbedf595f62e943a9f8b0ba9af5b1e61045c5f8f59c2c02c1212a16 AS runner WORKDIR /app ENV NODE_ENV=production ENV PORT=3000 From ec455625e046c55785e1df59e9a2823d7c7e12e8 Mon Sep 17 00:00:00 2001 From: SoundMindsAI Date: Tue, 2 Jun 2026 21:40:48 -0400 Subject: [PATCH 2/2] ci(security): builder stage inherits from deps (Gemini #431) Accept Gemini's finding on PR #431: have the ui builder stage `FROM deps` instead of re-deriving from the base image. deps already has pnpm, WORKDIR, and node_modules, so this drops a redundant `npm install -g pnpm@9` + the node_modules COPY, removes one base-image digest occurrence, and eliminates one of the two npmCommand Scorecard findings. A stage ref to a digest-pinned stage is still credited as pinned by Scorecard. node_modules is .dockerignore'd so `COPY . .` doesn't clobber the installed deps. Verified with a full `docker buildx build` of the ui image (next build + runner copy succeed). Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: SoundMindsAI --- ui/Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/Dockerfile b/ui/Dockerfile index 77d579df..af2454ce 100644 --- a/ui/Dockerfile +++ b/ui/Dockerfile @@ -28,10 +28,10 @@ RUN npm install -g pnpm@9 COPY pnpm-lock.yaml package.json ./ RUN pnpm install --frozen-lockfile -FROM node:26-bookworm-slim@sha256:79723b41edbedf595f62e943a9f8b0ba9af5b1e61045c5f8f59c2c02c1212a16 AS builder -WORKDIR /app -RUN npm install -g pnpm@9 -COPY --from=deps /app/node_modules ./node_modules +# Inherit from `deps` — pnpm, WORKDIR, and node_modules are already in place, +# so no redundant install/copy (and one fewer digest occurrence). A stage ref +# to a digest-pinned stage is still credited as pinned by Scorecard. +FROM deps AS builder COPY . . ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL