diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..93176183 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,44 @@ +# Dependabot keeps dependencies and CI actions patched. Three ecosystems: +# - npm → the pnpm workspace (Dependabot reads pnpm-lock.yaml v9) +# - github-actions → action version bumps (feeds the SHA-pin comments) +# - docker → the Dockerfile base image (FROM node:20-slim) +# +# NOTE (intentional): there is NO docker entry for docker-compose.yml. It sits +# at the same "/" directory (a second docker entry would collide on +# ecosystem+directory), and Dependabot's docker ecosystem parses Dockerfiles, +# not `image:` refs in compose. The pinned caddy / livekit-server compose images +# are updated MANUALLY — see the maintainer checklist in +# docs/systems/security-scanning.md. +version: 2 +updates: + - package-ecosystem: npm + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 10 + groups: + # One grouped PR for routine minor/patch bumps to cut PR noise. + npm-minor-patch: + update-types: + - minor + - patch + ignore: + # uiohook-napi is pinned by an exact-version pnpm patch + # (patches/uiohook-napi@1.5.5.patch). A bump makes the patch path stop + # matching, breaking `pnpm install --frozen-lockfile` in CI and both + # Docker stages until the patch is regenerated. Bump it by hand. + - dependency-name: uiohook-napi + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + groups: + github-actions: + patterns: + - "*" + + - package-ecosystem: docker + directory: / + schedule: + interval: weekly diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f78b24f..9c8fe40a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,15 +31,20 @@ jobs: node-version: [20, 24] steps: + - name: Harden the runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Setup pnpm - uses: pnpm/action-setup@v5 + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 with: version: 10.34.3 - name: Setup Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 with: node-version: ${{ matrix.node-version }} cache: pnpm diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index d17158e2..132e8b5b 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -29,7 +29,7 @@ jobs: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' - uses: contributor-assistant/github-action@v2.6.1 + uses: contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..4c941250 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,48 @@ +name: CodeQL + +# Static application security testing for all TS/JS. Uses build-mode: none — no +# compile needed, which sidesteps the monorepo/native-module build entirely. +# Default (code-scanning) query suite; security-extended is deferred (triage tax). +# CodeQL uploads alerts to the Security tab but does NOT fail the PR by itself — +# blocking is a repo setting (code-scanning merge protection), documented in the +# maintainer checklist in docs/systems/security-scanning.md. + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: '27 3 * * 1' # weekly Monday 03:27 UTC + +permissions: + contents: read + +concurrency: + group: codeql-${{ github.ref }} + cancel-in-progress: true + +jobs: + analyze: + name: Analyze (javascript-typescript) + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write # upload SARIF to code scanning + actions: read + steps: + - name: Harden the runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + - name: Checkout + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - name: Initialize CodeQL + uses: github/codeql-action/init@02c5e83432fe5497fd85b873b6c9f16a8578e1d9 # v3.37.0 + with: + languages: javascript-typescript + build-mode: none + - name: Perform CodeQL analysis + uses: github/codeql-action/analyze@02c5e83432fe5497fd85b873b6c9f16a8578e1d9 # v3.37.0 + with: + category: "/language:javascript-typescript" diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index e68102dc..fba9cc0f 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -24,10 +24,10 @@ jobs: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - - uses: actions/checkout@v5 - - uses: actions/configure-pages@v6 - - uses: actions/upload-pages-artifact@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 + - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 with: path: ./site - id: deployment - uses: actions/deploy-pages@v5 + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 4c85d13d..aff09f28 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -33,7 +33,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 # The runtime image bakes the git commit for the AGPL-3.0 § 13 source # offer (config.commit → GET /api/instance/info). The .git dir is not in @@ -44,13 +44,13 @@ jobs: run: echo "commit=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: registry: ghcr.io username: ${{ github.actor }} @@ -58,7 +58,7 @@ jobs: - name: Derive image tags and labels id: docker_meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 with: # github.repository is "TheZwiss/backspace"; metadata-action lowercases # it → ghcr.io/thezwiss/backspace (GHCR requires lowercase). @@ -77,7 +77,7 @@ jobs: org.opencontainers.image.revision=${{ github.sha }} - name: Build and push (linux/amd64, linux/arm64) - uses: docker/build-push-action@v6 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: context: . platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b36e9d67..f0de0122 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,8 +34,14 @@ jobs: runs-on: ${{ matrix.os }} steps: + - name: Harden the runner + if: runner.os == 'Linux' + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Install Linux build dependencies if: runner.os == 'Linux' @@ -65,12 +71,12 @@ jobs: sudo gem install --no-document fpm - name: Setup pnpm - uses: pnpm/action-setup@v5 + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 with: version: 10.34.3 - name: Setup Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 with: node-version: 20 cache: pnpm diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 00000000..bdda8659 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,48 @@ +name: OpenSSF Scorecard + +# Scores the repo's security posture (branch protection, pinned deps, token +# permissions, etc.) and publishes to the OpenSSF public API so a badge can be +# shown (badge is added in Plan E). REQUIRES the canonical repo to be PUBLIC — +# see the maintainer checklist in docs/systems/security-scanning.md. + +on: + branch_protection_rule: + schedule: + - cron: '18 4 * * 2' # weekly Tuesday 04:18 UTC + push: + branches: [main] + +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + security-events: write # upload SARIF + id-token: write # publish_results OIDC attestation + steps: + - name: Harden the runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + - name: Checkout + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + with: + persist-credentials: false + - name: Run Scorecard + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + - name: Upload artifact + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + - name: Upload SARIF to code scanning + uses: github/codeql-action/upload-sarif@02c5e83432fe5497fd85b873b6c9f16a8578e1d9 # v3.37.0 + with: + sarif_file: results.sarif diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 00000000..5f457881 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,123 @@ +name: Security + +# Report-only in this plan: every scanner is non-blocking and uploads SARIF to +# the Security tab. Enforcement (fail on fixable HIGH/CRITICAL, block on secrets) +# is flipped on in Plan E after the remediation pass. + +on: + pull_request: + push: + branches: [main] + schedule: + - cron: '32 5 * * 1' # weekly Monday 05:32 UTC + +permissions: + contents: read + +concurrency: + group: security-${{ github.ref }} + cancel-in-progress: true + +jobs: + gitleaks: + name: Secret scan (gitleaks) + runs-on: ubuntu-latest + steps: + - name: Harden the runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + - name: Checkout (full history) + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + with: + fetch-depth: 0 # gitleaks scans the whole git history, not just the diff + - name: Run gitleaks + uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2.3.9 + continue-on-error: true # report-only; enforcement flipped on in Plan E + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + osv-scanner: + name: Dependency scan (OSV-Scanner) + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write # upload SARIF to code scanning + steps: + - name: Harden the runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + - name: Checkout + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - name: Run OSV-Scanner + uses: google/osv-scanner-action/osv-scanner-action@9a498708959aeaef5ef730655706c5a1df1edbc2 # v2.3.8 + continue-on-error: true # report-only; enforcement flipped on in Plan E + with: + scan-args: |- + --lockfile=./pnpm-lock.yaml + --format=sarif + --output=osv-results.sarif + - name: Upload OSV SARIF + if: always() + uses: github/codeql-action/upload-sarif@02c5e83432fe5497fd85b873b6c9f16a8578e1d9 # v3.37.0 + with: + sarif_file: osv-results.sarif + category: osv-scanner + + trivy-config: + name: IaC/config scan (Trivy) + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + steps: + - name: Harden the runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + - name: Checkout + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - name: Trivy config scan (Dockerfile + docker-compose) + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + continue-on-error: true # report-only; enforcement flipped on in Plan E + with: + scan-type: config + scan-ref: . + format: sarif + output: trivy-config.sarif + - name: Upload Trivy config SARIF + if: always() + uses: github/codeql-action/upload-sarif@02c5e83432fe5497fd85b873b6c9f16a8578e1d9 # v3.37.0 + with: + sarif_file: trivy-config.sarif + category: trivy-config + + trivy-license: + name: License compliance scan (Trivy) + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + steps: + - name: Harden the runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + - name: Checkout + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - name: Trivy license scan + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + continue-on-error: true # report-only; enforcement flipped on in Plan E + with: + scan-type: fs + scan-ref: . + scanners: license + format: sarif + output: trivy-license.sarif + - name: Upload Trivy license SARIF + if: always() + uses: github/codeql-action/upload-sarif@02c5e83432fe5497fd85b873b6c9f16a8578e1d9 # v3.37.0 + with: + sarif_file: trivy-license.sarif + category: trivy-license diff --git a/CLAUDE.md b/CLAUDE.md index e9e72ba2..76fca0e7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -160,6 +160,7 @@ Before modifying any subsystem, read its spec from `docs/systems/`. After making | [message-list.md](docs/systems/message-list.md) | Auto-scroll model, position memory (session-only), embed renderer dimension contract, known limitations | Touching MessageList.tsx, scroll behavior, embed renderers, position restore | | [deployment.md](docs/systems/deployment.md) | Hosting pipeline: Docker/Caddy build, admin bootstrap, DB backup/restore, image pinning, env vars | Any deploy, backup/restore, or hosting change | | [activity-presence.md](docs/systems/activity-presence.md) | Presence states, rich activities, activity types/priorities, broadcast pipeline, visibility control, ActivityCard/Panel | Presence, rich activities, activity display, status management | +| [security-scanning.md](docs/systems/security-scanning.md) | CI security pipeline: Dependabot, CodeQL SAST, gitleaks, OSV-Scanner, Trivy (config/license; image scan in a later plan), OpenSSF Scorecard, SHA-pinning, harden-runner, tiered enforcement policy, maintainer settings checklist | Any CI security work, adding/changing scanners, enabling enforcement, supply-chain hardening | --- diff --git a/docs/systems/security-scanning.md b/docs/systems/security-scanning.md new file mode 100644 index 00000000..104bdb31 --- /dev/null +++ b/docs/systems/security-scanning.md @@ -0,0 +1,55 @@ +# Security Scanning & Supply-Chain Assurance + +Automated, continuous scanning wired into GitHub Actions. This document is the +reference for what runs, where results go, and the one-time settings a maintainer +must enable. **Current state: report-only** — scanners surface findings in the +Security tab but do not block merges yet. Enforcement (blocking) is turned on in a +later change once the remediation pass has cleared the backlog. + +## Workflows + +| File | Purpose | Trigger | Result | +|------|---------|---------|--------| +| `.github/dependabot.yml` | Dependency + action + base-image update PRs | weekly | PRs | +| `.github/workflows/codeql.yml` | CodeQL SAST (`javascript-typescript`, build-mode none) | PR + push main + weekly | Security tab | +| `.github/workflows/security.yml` | gitleaks (secrets, full history), OSV-Scanner (deps), Trivy config (IaC), Trivy license | PR + push main + weekly | Security tab | +| `.github/workflows/scorecard.yml` | OpenSSF Scorecard (repo posture) | push main + weekly + on branch-protection change | Security tab + public badge | + +> **gitleaks findings** surface in the workflow's job log and PR summary — the +> `gitleaks` job does not upload SARIF, so secret hits do **not** appear under +> Security → Code scanning (unlike the OSV / Trivy / CodeQL / Scorecard jobs). + +## Tiered policy (target, enforced in a later change) + +- **Always block:** gitleaks secret hit; OSV/Trivy fixable HIGH/CRITICAL; Trivy + disallowed license. +- **Advisory (SARIF → Security tab):** CodeQL alerts; OSV/Trivy unfixable or + medium/low; Scorecard. + +Code-level gates (OSV, Trivy, gitleaks) block via workflow exit codes. CodeQL +merge-blocking, Dependabot alerts, and native secret-scanning are GitHub *settings* +— see the checklist below. + +## Supply-chain hardening + +- Every action is pinned to a full commit SHA (`# vX.Y.Z` comment) — resists + tag-move attacks and satisfies Scorecard's Pinned-Dependencies check. +- `step-security/harden-runner` (egress-policy `audit`) on Linux jobs. +- Least-privilege `permissions:` per workflow/job. +- SBOM + SLSA provenance **will be** attached to the published container image + (added with the container-image-scan work in a later plan — not yet live). + +## Maintainer checklist (one-time GitHub settings — NOT code) + +- [ ] Repository must be **public** (required for the Scorecard badge/publish and + the CodeQL free tier). +- [ ] Settings → Code security: enable **Dependabot alerts** and **Dependabot + security updates**. +- [ ] Settings → Code security: enable **Secret scanning** + **Push protection**. +- [ ] Settings → Code security: enable **CodeQL / code-scanning merge protection** + so high-severity alerts block PRs (the code-level gates do the rest). +- [ ] Branch protection on `main`: require the CI + security status checks to pass. +- [ ] **Manual image bumps:** Dependabot does not track `docker-compose.yml` + `image:` pins — update `caddy` and `livekit/livekit-server` by hand when new + releases ship. (Renovate, which parses compose, is an optional future + alternative.)