Add CI security scanning (#14) #37

Merged
james merged 10 commits from 14-ci-security-scanning into main 2026-06-14 00:18:27 +00:00
Owner

Closes #14.

Summary

  • Three new PR-pipeline jobs in .forgejo/workflows/pr.yml:
    • npm_audit — production deps only (--omit=dev)
    • osv_scan — OSV-Scanner v1.9.2 binary, pinned, against package-lock.json
    • image_scan — Trivy 0.58.1 against a locally-built carol-service:scan image (uses the :act-24.04 runner image because it ships the Docker CLI)
  • A single configurable knob: workflow-level SECURITY_SEVERITY_THRESHOLD: high. All three scanners read it through scripts/ci/security-summary.mjs, which normalizes each scanner's severity scale (npm's moderatemedium, OSV's CVSS-score fallback, Trivy's uppercase) into one four-level scale.
  • Findings render as a Markdown table on the workflow run page via $GITHUB_STEP_SUMMARY — count by severity, then per-finding rows with package, advisory link, installed/range, fix version. Not buried in raw step logs.
  • ADR-0004 records tool choices, the high threshold rationale, and alternatives considered (Snyk, audit-ci, Grype, CodeQL, per-scanner native flags as the gate, warn-only mode).

Bundled: Kysely 0.27.6 → 0.29.2

npm audit against the current lockfile flags Kysely with three high-severity SQL-injection advisories (GHSA-wmrf-hv6w-mr66, GHSA-8cpq-38p9-67gx, GHSA-pv5w-4p9q-p3v2). Without the bump the new gate would block every PR after this one lands. Fix lands here so #14 ships in a coherent state.

Kysely 0.29 moved Migrator and Migration types to a kysely/migration subpath; db/migrator.ts and db/migrations/001_example.ts follow the new import.

Branch-name note

The original branch 19-ci-security-scanning was mis-numbered. This PR is on 14-ci-security-scanning (matches the ticket).

Test plan

  • All seven jobs (lint, typecheck, build, test×2, npm_audit, osv_scan, image_scan) go green on this PR
  • Workflow run page renders the three security summaries as Markdown tables, each showing 0 findings at/above high on a clean tree
  • (Manual follow-up on a throwaway branch) npm install lodash@4.17.20 trips both npm_audit and osv_scan with populated tables; revert
  • (Manual) bumping the env to SECURITY_SEVERITY_THRESHOLD: critical retunes all three gates without further edits

🤖 Generated with Claude Code

Closes #14. ## Summary - Three new PR-pipeline jobs in `.forgejo/workflows/pr.yml`: - `npm_audit` — production deps only (`--omit=dev`) - `osv_scan` — OSV-Scanner v1.9.2 binary, pinned, against `package-lock.json` - `image_scan` — Trivy 0.58.1 against a locally-built `carol-service:scan` image (uses the `:act-24.04` runner image because it ships the Docker CLI) - A single configurable knob: workflow-level `SECURITY_SEVERITY_THRESHOLD: high`. All three scanners read it through `scripts/ci/security-summary.mjs`, which normalizes each scanner's severity scale (npm's `moderate` → `medium`, OSV's CVSS-score fallback, Trivy's uppercase) into one four-level scale. - Findings render as a Markdown table on the workflow run page via `$GITHUB_STEP_SUMMARY` — count by severity, then per-finding rows with package, advisory link, installed/range, fix version. Not buried in raw step logs. - ADR-0004 records tool choices, the `high` threshold rationale, and alternatives considered (Snyk, audit-ci, Grype, CodeQL, per-scanner native flags as the gate, warn-only mode). ## Bundled: Kysely 0.27.6 → 0.29.2 `npm audit` against the current lockfile flags Kysely with three high-severity SQL-injection advisories (GHSA-wmrf-hv6w-mr66, GHSA-8cpq-38p9-67gx, GHSA-pv5w-4p9q-p3v2). Without the bump the new gate would block every PR after this one lands. Fix lands here so #14 ships in a coherent state. Kysely 0.29 moved `Migrator` and `Migration` types to a `kysely/migration` subpath; `db/migrator.ts` and `db/migrations/001_example.ts` follow the new import. ## Branch-name note The original branch `19-ci-security-scanning` was mis-numbered. This PR is on `14-ci-security-scanning` (matches the ticket). ## Test plan - [ ] All seven jobs (lint, typecheck, build, test×2, npm_audit, osv_scan, image_scan) go green on this PR - [ ] Workflow run page renders the three security summaries as Markdown tables, each showing 0 findings at/above `high` on a clean tree - [ ] (Manual follow-up on a throwaway branch) `npm install lodash@4.17.20` trips both `npm_audit` and `osv_scan` with populated tables; revert - [ ] (Manual) bumping the env to `SECURITY_SEVERITY_THRESHOLD: critical` retunes all three gates without further edits 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Add CI security scanning (#14)
Some checks failed
PR / Test (postgres) (pull_request) Failing after 2m31s
PR / npm audit (pull_request) Successful in 19s
PR / Test (sqlite) (pull_request) Failing after 3m16s
PR / Typecheck (pull_request) Failing after 1m17s
PR / Lint (pull_request) Failing after 1m53s
PR / Build (pull_request) Failing after 2m16s
PR / OSV-Scanner (pull_request) Failing after 2m48s
PR / Trivy (image) (pull_request) Failing after 3m29s
ef36b5f24e
Adds three security jobs to the PR workflow: npm_audit and osv_scan run
against the production lockfile, image_scan builds the service image and
runs Trivy against it. A single SECURITY_SEVERITY_THRESHOLD env var (set
to `high`) is the gate for all three — each scanner's JSON is normalized
through scripts/ci/security-summary.mjs, which writes a Markdown findings
table to $GITHUB_STEP_SUMMARY and prints qualifying=<n> for the gate.

Bundles a Kysely 0.27.6 → 0.29.2 bump that closes a high-severity SQL-
injection advisory (GHSA-wmrf-hv6w-mr66 et al.) so PRs land green once
the new gate is enforced. Kysely 0.29 moved Migrator/Migration types to
the kysely/migration subpath; the migrator and the example migration
follow the new import.

Tool choices and the `high` threshold are recorded in ADR-0004.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pull CI actions from the local forgejo mirror
Some checks failed
PR / Lint (pull_request) Failing after 3s
PR / Build (pull_request) Failing after 4s
PR / Trivy (image) (pull_request) Failing after 4s
PR / OSV-Scanner (pull_request) Failing after 7s
PR / Test (sqlite) (pull_request) Failing after 8s
PR / Test (postgres) (pull_request) Failing after 9s
PR / Typecheck (pull_request) Failing after 3s
PR / npm audit (pull_request) Failing after 6s
914bfb8e4d
The act_runner couldn't resolve actions/checkout@v4 and actions/setup-node@v4
via data.forgejo.org, breaking every job on PR #37. Switch all uses: to the
local mirror at forge.wynning.tech/actions/* so the runner pulls from a
source we control.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Use full https:// URL for the mirrored action sources
Some checks failed
PR / Trivy (image) (pull_request) Failing after 58s
PR / OSV-Scanner (pull_request) Failing after 1m2s
PR / Lint (pull_request) Failing after 1m14s
PR / Build (pull_request) Failing after 1m34s
PR / Test (postgres) (pull_request) Successful in 1m40s
PR / Test (sqlite) (pull_request) Successful in 1m42s
PR / npm audit (pull_request) Successful in 1m42s
PR / Typecheck (pull_request) Failing after 1m54s
d35f6b34e6
Without the scheme, act_runner treats the host as a relative path and
fails to resolve the action.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bump pinned action tags to current releases
Some checks failed
PR / Trivy (image) (pull_request) Failing after 11s
PR / OSV-Scanner (pull_request) Failing after 18s
PR / Lint (pull_request) Successful in 1m53s
PR / Test (sqlite) (pull_request) Successful in 1m57s
PR / Test (postgres) (pull_request) Successful in 1m57s
PR / Typecheck (pull_request) Successful in 1m59s
PR / npm audit (pull_request) Successful in 1m58s
PR / Build (pull_request) Successful in 2m12s
343147bd4b
Move from @v4 floating to specific tags that the act_runner can resolve
cleanly from the local Forgejo mirror — actions/checkout@v6.0.3 and
actions/setup-node@v6.4.0.

(Restores actions/checkout on the first uses: in each job; the prior edit
duplicated setup-node by mistake.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fix scanner install URLs and bump to current stable tags
Some checks failed
PR / OSV-Scanner (pull_request) Successful in 10m58s
PR / Typecheck (pull_request) Has been cancelled
PR / Lint (pull_request) Has been cancelled
PR / Build (pull_request) Has been cancelled
PR / npm audit (pull_request) Has been cancelled
PR / Test (sqlite) (pull_request) Has been cancelled
PR / Test (postgres) (pull_request) Has been cancelled
PR / Trivy (image) (pull_request) Has been cancelled
990dede20a
OSV-Scanner: the v1.9.2 asset URL embedded the version in the filename
(404). The actual asset name is just `osv-scanner_linux_amd64` with no
version suffix. Bumping to v2.3.8 (current) at the same time and
switching the CLI to the v2 `scan source` subcommand.

Trivy: v0.58.1 didn't exist as a release (the install.sh fell back to
"latest" but with the requested tag baked into the URL, producing an
error 1). Bumping to v0.71.0 and replacing the install.sh indirection
with a direct tarball fetch — fewer moving parts, the script's behaviour
drifted between tags.

OSV-Scanner v2 surfaces dev-only deps that npm audit hides via
--omit=dev, so the summary helper now reads `dependency_groups` and
suppresses packages whose only group is `dev`. Matches the npm_audit
policy; ADR-0004 updated to record the rationale and the resulting
gap in coverage of the developer-machine threat model.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Renumber ADR-0004 → ADR-0005
Some checks failed
PR / Lint (pull_request) Failing after 59s
PR / Typecheck (pull_request) Failing after 1m26s
PR / Build (pull_request) Failing after 2m2s
PR / Test (postgres) (pull_request) Failing after 2m26s
PR / Test (sqlite) (pull_request) Failing after 3m27s
PR / Trivy (image) (pull_request) Failing after 3m32s
PR / npm audit (pull_request) Failing after 4m9s
PR / OSV-Scanner (pull_request) Failing after 4m40s
600d497da6
#11 (local user auth) is also targeting ADR-0004; renumber this one
out of the way. File renamed via `git mv` to preserve history.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stop using uses: actions, inline checkout instead
Some checks failed
PR / Typecheck (pull_request) Has been cancelled
PR / Test (sqlite) (pull_request) Has been cancelled
PR / Build (pull_request) Has been cancelled
PR / Test (postgres) (pull_request) Has been cancelled
PR / Lint (pull_request) Has been cancelled
PR / Trivy (image) (pull_request) Has been cancelled
PR / OSV-Scanner (pull_request) Has been cancelled
PR / npm audit (pull_request) Has been cancelled
9a1d3b3728
act_runner pulls each `uses:` action by `git fetch origin <ref>` with
no shallow option. The Cloudflare-fronted Forgejo mirror at
forge.wynning.tech can't stream the resulting multi-MB packfile reliably
under CF's free-tier limits — index-pack exits 128 mid-stream. Two
attempts at fixing this in-mirror (current tags, pinned tags) both hit
the same failure.

Workaround: drop every `uses:` and inline what they did.
- Container image switches from `ghcr.io/catthehacker/ubuntu:js-24.04`
  to `node:22-slim` for all node-only jobs. The slim image ships Node
  22 directly, so setup-node becomes unnecessary; we apt-install git,
  curl, and ca-certificates per job because slim is bare-bones.
- Checkout is `git init` → `git fetch --depth=1 origin $head_sha` →
  `git checkout -f FETCH_HEAD`. The PR head SHA comes from the event
  payload, so this works for forks and same-repo PRs alike.
- image_scan keeps `:act-24.04` (still needs the Docker CLI it ships).

Trade-offs: lose setup-node's npm cache (every job does a full npm ci);
lose actions/checkout's submodule/lfs handling (we don't use either).

Revert when the mirror moves off CF or act_runner gains a shallow-fetch
option for action resolution. The file's top comment carries that note
so the workaround doesn't ossify.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Revert "Stop using uses: actions, inline checkout instead"
Some checks failed
PR / Test (sqlite) (pull_request) Has been cancelled
PR / Lint (pull_request) Has been cancelled
PR / Build (pull_request) Has been cancelled
PR / Trivy (image) (pull_request) Has been cancelled
PR / OSV-Scanner (pull_request) Has been cancelled
PR / npm audit (pull_request) Has been cancelled
PR / Test (postgres) (pull_request) Has been cancelled
PR / Typecheck (pull_request) Has been cancelled
e45029357d
This reverts commit 9a1d3b3728.
Pull actions over the CF-bypassing internal hostname
Some checks failed
PR / Lint (pull_request) Failing after 10s
PR / Build (pull_request) Failing after 7s
PR / Typecheck (pull_request) Failing after 10s
PR / OSV-Scanner (pull_request) Failing after 13s
PR / npm audit (pull_request) Failing after 16s
PR / Test (postgres) (pull_request) Failing after 20s
PR / Test (sqlite) (pull_request) Failing after 24s
PR / Trivy (image) (pull_request) Failing after 22s
6896ab0737
The public hostname forge.wynning.tech is Cloudflare-fronted, which
truncates the full-history pack act_runner fetches for each `uses:`
action (index-pack exits 128). forge.int.wynning.tech reaches the
same Forgejo instance directly and serves the same repos.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Roll back to default action resolution
Some checks failed
PR / OSV-Scanner (pull_request) Successful in 2m11s
PR / Trivy (image) (pull_request) Failing after 2m13s
PR / Lint (pull_request) Successful in 3m38s
PR / Test (sqlite) (pull_request) Successful in 3m46s
PR / Typecheck (pull_request) Successful in 3m47s
PR / npm audit (pull_request) Successful in 3m48s
PR / Test (postgres) (pull_request) Successful in 3m48s
PR / Build (pull_request) Successful in 4m3s
4cddb18b03
Runner-side networking fix landed out of band; the explicit
forge.int.wynning.tech prefix is no longer needed and the unqualified
`actions/checkout@v6.0.3` / `actions/setup-node@v6.4.0` form lets the
runner resolve actions through its default source again.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add .trivyignore for unfixable Debian base + bundled Next.js CVEs
All checks were successful
PR / OSV-Scanner (pull_request) Successful in 17s
PR / Trivy (image) (pull_request) Successful in 54s
PR / npm audit (pull_request) Successful in 1m46s
PR / Typecheck (pull_request) Successful in 1m56s
PR / Test (postgres) (pull_request) Successful in 2m0s
PR / Test (sqlite) (pull_request) Successful in 2m0s
PR / Lint (pull_request) Successful in 2m0s
PR / Build (pull_request) Successful in 2m9s
93829530d5
Trivy's first run flagged 11 findings at/above `high`. None are
actionable today:

- 10 are Debian-bookworm OS packages in node:22-slim with `fix: none`
  upstream (perl-base ×6, zlib1g ×1, ncurses ×3). The vulnerable code
  paths aren't reachable from the running service (no perl invocation,
  no untrusted archive decompression, no TTY/curses).
- 1 is picomatch ≤4.0.3, bundled inside `next/dist/compiled/picomatch`.
  Not in our dep tree; only bumpable via a Next major release. Next
  uses it on developer-defined route patterns, not user input.

Each entry in .trivyignore carries a comment block with the rationale
and a "revisit when" trigger. Trivy invocation now passes
`--ignorefile .trivyignore` explicitly so the allowlist source is
self-evident in the job log (it's also the default path).

ADR-0005 updated: the allowlist mechanism the doc anticipated is now
in place for Trivy. Same pattern will extend to npm_audit / osv_scan
the first time one of them hits a non-actionable finding.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
james force-pushed 14-ci-security-scanning from 93829530d5
All checks were successful
PR / OSV-Scanner (pull_request) Successful in 17s
PR / Trivy (image) (pull_request) Successful in 54s
PR / npm audit (pull_request) Successful in 1m46s
PR / Typecheck (pull_request) Successful in 1m56s
PR / Test (postgres) (pull_request) Successful in 2m0s
PR / Test (sqlite) (pull_request) Successful in 2m0s
PR / Lint (pull_request) Successful in 2m0s
PR / Build (pull_request) Successful in 2m9s
to f6a810106b
All checks were successful
PR / OSV-Scanner (pull_request) Successful in 17s
PR / Lint (pull_request) Successful in 1m21s
PR / npm audit (pull_request) Successful in 1m37s
PR / Test (postgres) (pull_request) Successful in 1m37s
PR / Typecheck (pull_request) Successful in 1m57s
PR / Test (sqlite) (pull_request) Successful in 1m58s
PR / Build (pull_request) Successful in 2m26s
PR / Trivy (image) (pull_request) Successful in 2m38s
2026-06-14 00:15:18 +00:00
Compare
james merged commit aa309e186d into main 2026-06-14 00:18:27 +00:00
Sign in to join this conversation.
No description provided.