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..af2454ce 100644 --- a/ui/Dockerfile +++ b/ui/Dockerfile @@ -16,27 +16,28 @@ # 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 -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 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