diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3d0a8f..35e0126 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ permissions: jobs: ci: name: Lint, build, verify - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 10 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -31,8 +31,9 @@ jobs: bun-version: 1.3.13 # Bun is the package manager and script runner, but Next.js (and tsc) - # run on Node. ubuntu-latest's default Node version drifts; pin via - # .nvmrc so a future GitHub bump can't break the build silently. + # run on Node. The runner image is pinned (ubuntu-24.04) but Node + # version inside it can still drift; pin via .nvmrc so a future + # GitHub bump can't break the build silently. - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version-file: .nvmrc @@ -153,12 +154,12 @@ jobs: # Runs only on PRs (no baseline diff to compute on a push to main). # Compares the PR's dependency manifest against main and flags - # high-severity advisories or license incompatibilities. Posts a summary - # comment on the PR when it finds something. continue-on-error while we - # establish a baseline of acceptable findings. + # high-severity advisories or license incompatibilities. Hard-gated to + # match the `bun audit` posture: a PR introducing a new high-severity + # advisory must block merge, not just post a comment. dependency-review: if: github.event_name == 'pull_request' - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: contents: read pull-requests: write @@ -166,7 +167,33 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Dependency Review uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0 - continue-on-error: true with: fail-on-severity: high comment-summary-in-pr: on-failure + + # Defense-in-depth on top of GitHub's push protection. Push protection + # covers high-entropy / known-provider patterns at push time; TruffleHog + # re-scans diffs on PRs + push to main, catching secrets that slipped + # past push protection (low-entropy formats, detector patterns added + # after the secret was committed, or push-protection bypass via the + # "I'll fix it later" allow path). + secret-scan: + runs-on: ubuntu-24.04 + permissions: + contents: read + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + # TruffleHog diffs base..head and needs the full history present. + fetch-depth: 0 + - name: TruffleHog scan + uses: trufflesecurity/trufflehog@17456f8c7d042d8c82c9a8ca9e937231f9f42e26 # v3.95.2 + with: + # On PRs: scan the diff between base and head. On push to main: + # scan the previous commit to HEAD. The action infers both from + # the event context. + # --results=verified,unknown drops trufflehog's "unverified" tier + # (pattern-matched but couldn't reach the verifier endpoint) to + # keep noise down without losing coverage of secrets whose + # verifiers don't recognize them yet. + extra_args: --results=verified,unknown diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 4b90b53..aef1eaf 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -32,7 +32,7 @@ jobs: (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 30 permissions: contents: write diff --git a/.github/workflows/lighthouse.yml b/.github/workflows/lighthouse.yml index 550d6b7..868e922 100644 --- a/.github/workflows/lighthouse.yml +++ b/.github/workflows/lighthouse.yml @@ -17,7 +17,7 @@ concurrency: jobs: lighthouse: name: Lighthouse audit - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 15 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -27,8 +27,9 @@ jobs: bun-version: 1.3.13 # Bun is the package manager and script runner, but Next.js (and the - # lhci binary) run on Node. Pin Node via .nvmrc so a future GitHub - # bump can't break the audit silently. + # lhci binary) run on Node. The runner image is pinned (ubuntu-24.04) + # but Node version inside it can still drift; pin via .nvmrc so a + # future GitHub bump can't break the audit silently. - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version-file: .nvmrc diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 0000000..d07c57c --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,88 @@ +name: Scorecard + +# OpenSSF Scorecard — weekly supply-chain hygiene grade for this repo. +# Checks include: branch protection, pinned dependencies, signed releases, +# token permissions, dangerous workflow patterns, dependency-update +# tooling, vulnerability backlog, and code review coverage. Results post +# to GitHub's Security tab as SARIF and (when publish_results is on) get +# published to the public Scorecard registry at https://scorecard.dev. +# +# Required setup (one-time): +# - Settings → Code security → Enable "Code scanning" so the SARIF +# upload has somewhere to land. Without it the upload step no-ops. +# +# Runs on: +# - branch_protection_rule events (re-grades whenever protection changes) +# - schedule (weekly Monday 08:00 UTC — keeps the grade current as +# dependencies and Actions versions drift) +# - push to main (catches workflow changes immediately) +# - workflow_dispatch (manual re-run when investigating a finding) +# +# Note on PRs: Scorecard is intentionally NOT triggered on `pull_request`. +# Every check Scorecard runs (branch protection state, pinned dependencies +# in main, code-review-coverage history, vulnerability backlog) reflects +# `main` as it currently sits — running on a PR would just re-grade main, +# not preview the PR's effect. The weekly schedule + push-to-main triggers +# are the right cadence. + +on: + branch_protection_rule: + schedule: + - cron: "0 8 * * 1" + push: + branches: [main] + workflow_dispatch: + +# Cancel an in-flight schedule/push run if a new one starts on the same ref. +concurrency: + group: scorecard-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# Least-privilege default; the job opts up explicitly for SARIF upload + +# OIDC publish. +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-24.04 + permissions: + # Needed by github/codeql-action/upload-sarif to write Security tab + # entries. + security-events: write + # Needed by scorecard's `publish_results: true` — OIDC token proves + # to scorecard.dev that the run came from this repo. + id-token: write + contents: read + # Required for org-level scorecard repo discovery (no-op for a + # single-repo case, but the action documents it as required). + actions: read + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Run Scorecard + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 + with: + results_file: results.sarif + results_format: sarif + # Publish to https://scorecard.dev so the badge + history are + # visible. Set to false to keep results private to the + # Security tab. + publish_results: true + + # Retained for 7 days as a fallback when the Security tab view is + # truncated or the SARIF upload fails. + - name: Upload artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: SARIF file + path: results.sarif + retention-days: 7 + + - name: Upload to code-scanning + uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + with: + sarif_file: results.sarif diff --git a/CLAUDE.md b/CLAUDE.md index 6f8c110..d79c08f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,11 +41,30 @@ This is a Next.js 15 marketing website for Vortex, a columnar file format. The s - `src/app/page.tsx` - Homepage with metadata for SEO - `src/components/hero/index.tsx` - Complex WebGL rendering with custom shaders - `src/app/layout.tsx` - Root layout with analytics providers and font loading -- `next.config.ts` - Plausible proxy configuration +- `next.config.mjs` - Plausible proxy configuration, security headers (CSP, HSTS, Permissions-Policy) The site is optimized for performance with font optimization, analytics integration, and responsive WebGL rendering. -## Audit advisories +## Supply chain hardening + +Defense in depth against the npm-worm class (Shai-Hulud, mini-Shai-Hulud, the May 2026 TanStack incident, etc.). Layers, in order of which one trips first when a bad package surfaces: + +1. **`bunfig.toml` → `install.minimumReleaseAge = 1209600`** (14 days). Bun refuses to resolve to a package version younger than 14 days. Applies to every `bun install` / `bun add` / `bun update` (local dev, lockfile regens, and Vercel builds), so we can't accidentally pull a freshly published version that hasn't had time to be observed. If a fresh dep is genuinely needed before the window elapses, allowlist it via `install.minimumReleaseAgeExcludes`. +2. **`renovate.json` → `minimumReleaseAge: "14 days"`** — symmetric with #1, applied at PR-proposal time. Renovate won't open a PR for a version younger than the window, and `bunfig.toml` won't let Bun resolve to one either. Both gates are required: a local `bun add` bypasses Renovate; a Renovate `lockFileMaintenance` cycle would otherwise pull fresh transitives. +3. **`trustedDependencies` in `package.json`** — explicit allowlist for which packages may run lifecycle scripts (`preinstall` / `install` / `postinstall`). Bun's default behavior is name-only trust against a built-in ~366-package allowlist, which lets a transitive named like a popular package hijack scripts (the PackageGate class of attack). Current set: `["esbuild", "sharp"]` — esbuild's `postinstall` builds its native bin (pulled in transitively via velite); sharp's `install` builds libvips for next/image. When adding a top-level dep that ships native bins or needs a build step, audit its lifecycle scripts via `bun pm untrusted`, then extend this list with one-line justification in the commit message. +4. **`bun audit`** is the CI hard gate on every PR and push to main. When a new advisory surfaces, the resolution is one of three: + - **Direct dep bump** — if the advisory is in a top-level dep with a patched release, bump in `package.json`. + - **`overrides` entry** — if the advisory is in a transitive dep whose parent hasn't released a fix, force-pin the patched version in `package.json`'s `overrides` block. This is the most common case. Pick the latest patched version that is ≥14 days old (matching the cooldown policy) so a fresh install can't resolve to a too-new version. + - **Document and ignore** — if no upstream fix exists yet, append `--ignore=GHSA-...` to the `Dependency audit` step in `.github/workflows/ci.yml` and add an entry under "Audit advisories" below with: GHSA ID, vulnerable range, package, why exposure is acceptable (e.g. dev-only, not in client bundle), and a removal trigger. + - When a parent dep eventually patches its own transitive, drop the corresponding `overrides` entry — leaving stale overrides means we keep deduping a fix that was already merged upstream. +5. **`actions/dependency-review-action`** runs on every PR — hard-fails the build if a PR introduces a new advisory at severity `high` or above. Catches what `bun audit` would catch on the merge commit, but earlier in the review loop. +6. **`trufflesecurity/trufflehog`** secret scan runs on every PR and push to main. Defense-in-depth on top of GitHub's push protection; catches verified secrets that slipped past push protection (low-entropy formats, detector patterns added after the secret was committed, or push-protection bypass). +7. **OpenSSF Scorecard** (`.github/workflows/scorecard.yml`) grades the repo weekly on supply-chain hygiene (pinned actions, branch protection, token permissions, dangerous workflow patterns). SARIF posts to the Security tab; aggregate score publishes to https://scorecard.dev. +8. **All third-party GitHub Actions are SHA-pinned**, not tag-pinned. A tag can be moved to point at a malicious commit (and has been, in prior supply-chain incidents); a SHA can't. Renovate's `helpers:pinGitHubActionDigests` preset enforces this on auto-bump PRs. When bumping an action manually, update both the SHA and the trailing `# vX.Y.Z` comment in the same diff. +9. **`runs-on: ubuntu-24.04`** (not `ubuntu-latest`) so runner-image bumps are deliberate PRs, not silent infrastructure drift. The Node version inside the runner image can still drift; `.nvmrc` pins that separately. +10. **Renovate auto-merge** is patch + minor only via `:automergeStableNonMajor`. Major bumps stay open for human review. The 14-day cooldown is the first gate; CI (lint, build, typecheck, `bun audit`, `dependency-review`, `secret-scan`, `bun run verify`, Playwright, Lighthouse) is the second. + +### Audit advisories `bun audit` is the source of truth for dependency advisories. State as of 2026-05-04: @@ -54,4 +73,4 @@ The site is optimized for performance with font optimization, analytics integrat - **uuid `<14.0.0`** (GHSA-w5hq-g745-h8pq, moderate missing buffer bounds in v3/v5/v6 when `buf` provided). **Upstream-blocked.** Two parent paths: `resend@6.12.2 → svix@1.90.0 → uuid@^10.0.0` and `@lhci/cli@0.15.1 → uuid@8.3.2`. Neither parent admits a 14.x override without risking CJS imports. Exposure is theoretical on both: `/api/subscribe` uses Resend's send-email endpoint (not svix's webhook-signing path), `@lhci/cli` is dev-only and runs in CI on its own controlled inputs, and the vulnerable code (v3/v5/v6 with explicit `buf`) isn't called by either. Remove the `--ignore` when both parents ship releases bumping uuid to `^14.0.0`. - **tmp `<=0.2.3`** (GHSA-52f5-9888-hmc6, low symbolic-link path traversal in `dir` param). **Upstream-blocked.** Pulled exclusively by `@lhci/cli@0.15.1` (dev-only, runs in CI on controlled inputs). The symlink-traversal scenario doesn't apply. Remove the `--ignore` when `@lhci/cli` ships a release with patched transitives. -CI hard-gates on `bun audit` (`.github/workflows/ci.yml`) with `--ignore=GHSA-w5hq-g745-h8pq` and `--ignore=GHSA-52f5-9888-hmc6` for the upstream-blocked advisories. Any new advisory fails the job. The `dependency-review-action` PR job is a separate gate (license/severity-focused) that remains `continue-on-error: true` while a baseline of acceptable findings is established. +CI hard-gates on `bun audit` (`.github/workflows/ci.yml`) with `--ignore=GHSA-w5hq-g745-h8pq` and `--ignore=GHSA-52f5-9888-hmc6` for the upstream-blocked advisories. Any new advisory fails the job. diff --git a/README.md b/README.md index f2d36c5..a9c101a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # vortex.dev +[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/vortex-data/vortex.dev/badge)](https://scorecard.dev/viewer/?uri=github.com/vortex-data/vortex.dev) + Source for [vortex.dev](https://vortex.dev), the marketing site for [Vortex](https://github.com/vortex-data/vortex) — an extensible, state-of-the-art columnar file format. Vortex is a [Linux Foundation](https://www.linuxfoundation.org/) incubating project, a Series of LF Projects, LLC. ## Local development diff --git a/bun.lock b/bun.lock index fa33bc6..10579fa 100644 --- a/bun.lock +++ b/bun.lock @@ -34,6 +34,10 @@ }, }, }, + "trustedDependencies": [ + "sharp", + "esbuild", + ], "overrides": { "mdast-util-to-hast": "^13.2.1", "postcss": "8.5.10", diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..33e9f81 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,16 @@ +# Bun configuration. Project-scoped — Bun merges this with $HOME/.bunfig.toml. + +[install] +# Refuse to install package versions younger than 14 days. Mirrors the +# Renovate minimumReleaseAge in renovate.json so a local `bun install`, +# `bun add`, `bun update`, or a Vercel build can't pull a freshly +# published (and potentially compromised) version that Renovate would +# have held back. +# +# The window matters because npm worms (Shai-Hulud, mini-Shai-Hulud, the +# May 2026 TanStack incident) propagate, get detected, and get yanked +# within hours-to-days; 14 days is the observed safe horizon for those +# events to have surfaced. +# +# Units: seconds. 14 * 86400 = 1209600. +minimumReleaseAge = 1209600 diff --git a/package.json b/package.json index 529f22b..d649250 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,10 @@ "tailwindcss": "4.2.3", "typescript": "^6.0.3" }, + "trustedDependencies": [ + "esbuild", + "sharp" + ], "overrides": { "postcss": "8.5.10", "mdast-util-to-hast": "^13.2.1" diff --git a/renovate.json b/renovate.json index 33536da..855583d 100644 --- a/renovate.json +++ b/renovate.json @@ -16,6 +16,7 @@ "enabled": true }, "automergeStrategy": "squash", + "minimumReleaseAge": "14 days", "rebaseWhen": "conflicted", "platformAutomerge": true, "labels": ["dependencies"],