Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions .github/scripts/audit-gate.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env node
// npm-audit-Gate mit begründeter Allowlist.
//
// Ersetzt das nackte `npm audit --audit-level=high`: blockt weiterhin bei JEDEM
// HIGH/CRITICAL-Advisory, LÄSST aber explizit begründete Ausnahmen durch. Gleiche
// Policy wie .trivyignore (dort für den Trivy-FS-Scan). npm audit selbst kann keine
// einzelnen Advisories ausnehmen — daher dieser Filter.
//
// Ausführung: aus frontend/ heraus -> node ../.github/scripts/audit-gate.mjs
import { execSync } from 'node:child_process';

// Begründete Ausnahmen: GHSA-ID -> Grund + Datum. Jede Zeile muss auch in .trivyignore
// gespiegelt sein (Trivy-Gate) und via Dependabot-Alert-Dismiss abgedeckt werden.
const ALLOWLIST = {
'GHSA-QWWW-VCR4-C8H2':
'react-router RSC-only CSRF (CWE-352); TaskWolf ist Client-SPA (createBrowserRouter, kein RSC/SSR) -> nicht ausnutzbar. Fix nur in react-router 8.3.0 (Major); v8-Migration geplant, 2026-07-29',
};

const BLOCK = new Set(['high', 'critical']);
const GHSA_RE = /GHSA-[a-z0-9]{4,}-[a-z0-9]{4,}-[a-z0-9]{4,}/i;

function runAudit() {
try {
return execSync('npm audit --json', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
} catch (e) {
// npm audit exit-code ist != 0, sobald Vulns existieren — das JSON liegt trotzdem auf stdout.
if (e.stdout) return e.stdout.toString();
throw e;
}
}

let audit;
try {
audit = JSON.parse(runAudit());
} catch (e) {
console.error('❌ audit-gate: npm-audit-JSON nicht parsebar:', e.message);
process.exit(1);
}

const vulns = audit.vulnerabilities || {};
const offending = new Map(); // GHSA-ID -> { pkg, severity, url }

for (const [pkg, info] of Object.entries(vulns)) {
if (!BLOCK.has(info.severity)) continue;
for (const via of info.via || []) {
// String-Einträge = transitive Weiterreichung eines anderen Pakets, kein eigenes
// Advisory -> überspringen (verhindert Doppelzählung/false-fail bei re-export-Paketen).
if (typeof via !== 'object' || via === null) continue;
const sev = String(via.severity || info.severity || '').toLowerCase();
if (!BLOCK.has(sev)) continue;
const m = String(via.url || '').match(GHSA_RE) || String(via.title || '').match(GHSA_RE);
const id = m ? m[0].toUpperCase() : (via.source != null ? `SOURCE-${via.source}` : `${via.name || pkg}-UNKNOWN`);
if (!(id in ALLOWLIST)) offending.set(id, { pkg: via.name || pkg, severity: sev, url: via.url });
}
}

if (offending.size > 0) {
console.error('❌ npm audit: nicht-allowlistete HIGH/CRITICAL-Advisories gefunden:');
for (const [id, o] of offending) console.error(` - ${id} (${o.severity}) in ${o.pkg} ${o.url || ''}`);
console.error('\nBehebe die Dependency ODER ergänze eine begründete Ausnahme in');
console.error('.github/scripts/audit-gate.mjs (ALLOWLIST) + .trivyignore + Dependabot-Alert-Dismiss.');
process.exit(1);
}

const allowed = Object.keys(ALLOWLIST);
console.log('✅ npm-audit-Gate ok: keine nicht-allowlisteten HIGH/CRITICAL-Advisories.');
if (allowed.length) console.log(` Aktive begründete Ausnahmen: ${allowed.join(', ')}`);
7 changes: 5 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,12 @@ jobs:
working-directory: frontend
run: npm ci

- name: npm audit (block on high/critical)
- name: npm audit (block on high/critical, begründete Allowlist)
working-directory: frontend
run: npm audit --audit-level=high
# Blockt bei jedem HIGH/CRITICAL-Advisory, lässt aber begründete Ausnahmen
# durch (Allowlist im Script, gespiegelt in .trivyignore). npm audit selbst
# kann keine einzelnen Advisories ausnehmen.
run: node ../.github/scripts/audit-gate.mjs

- name: i18n scanner self-tests
working-directory: frontend
Expand Down
7 changes: 7 additions & 0 deletions .trivyignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
# Begründete Ausnahmen. Format pro Zeile: CVE-ID # Grund + Datum
# z.B.: CVE-2024-12345 # nicht ausnutzbar (Komponente ungenutzt), 2026-06-28

# react-router GHSA-qwww-vcr4-c8h2 (CSRF, CWE-352, CVSS 7.1): betrifft AUSSCHLIESSLICH
# den unstable RSC-Modus (React Server Components). TaskWolf-Frontend ist eine reine
# Client-SPA (createBrowserRouter, kein SSR/RSC, keine unstable_ APIs) -> Code-Pfad
# ungenutzt, nicht ausnutzbar. Fix nur in react-router 8.3.0 (Major, react-router-dom
# in v8 aufgelöst). Auflösung geplant via v8-Migration -> docs/superpowers/plans/2026-07-29-react-router-v8-migration.md
GHSA-qwww-vcr4-c8h2 # RSC-only CSRF, Client-SPA nicht betroffen, 2026-07-29
86 changes: 86 additions & 0 deletions docs/superpowers/plans/2026-07-29-react-router-v8-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# React Router v7 → v8 Migration (Backlog)

**Status:** Backlog / geplant — noch NICHT umgesetzt
**Erstellt:** 2026-07-29
**Auslöser:** Security-Advisory `GHSA-qwww-vcr4-c8h2` (react-router, HIGH, CVSS 7.1)
**Aktueller Interim-Zustand:** CVE als *nicht ausnutzbar* suppressed an **drei** Gates
(2026-07-29): `.trivyignore` (Trivy-Nightly + ci.yml-Spiegel), `.github/scripts/audit-gate.mjs`
ALLOWLIST (npm-audit-Gate in ci.yml) und Dependabot-Alert #82 dismissed `not_used`.
Dieser Plan löst alle drei Ausnahmen sauber auf.

## Kontext / Warum

`GHSA-qwww-vcr4-c8h2` ist eine CSRF-Lücke (CWE-352) **ausschließlich im unstable
RSC-Modus** von React Router. Betroffene Range: `react-router >= 7.12.0 < 8.3.0`,
Fix nur in **8.3.0**. Kein v7-Patch existiert.

TaskWolf-Frontend ist eine **reine Client-SPA** (`createBrowserRouter` +
`RouterProvider`, Data-/Library-Mode, kein SSR/RSC, keine `unstable_`-APIs) →
der verwundbare Code-Pfad wird nicht genutzt, die CVE ist bei uns **nicht
ausnutzbar**. Deshalb ist die Migration eine geplante Aufräum-Aufgabe, keine
Notfall-Remediation.

`react-router-dom` hat **kein v8** (letzte Version 7.18.2, ebenfalls verwundbar):
das Paket wurde in v8 aufgelöst und in `react-router` zusammengeführt. Die Migration
ist damit unvermeidlich ein Major-Bump mit Import-Umstellung in 42 Dateien.

## Umfang (Ist-Stand main, verifiziert 2026-07-29)

- **42 Dateien** importieren aus `react-router-dom` (`frontend/src/**`).
- Genutzte Symbole (alle existieren in v8, keine dom-exklusiven APIs):
`Link, NavLink, Navigate, Outlet, RouterProvider, createBrowserRouter,
useMatch, useNavigate, useParams, useSearchParams`.
- Keine Loader/Actions/`meta`/`useMatches` → das v8-Rename `data → loaderData`
betrifft uns **nicht**.
- Kein Framework-Mode → v8-Future-Flags (`v8_middleware`, `v8_viteEnvironmentApi`,
Vite-Environment-API, Cloudflare-Plugin-Wechsel) betreffen uns **nicht**.

## v8-Breaking-Changes, die uns betreffen

1. **Paket entfernt:** `npm uninstall react-router-dom`, `react-router@^8.3.0` rein.
2. **Import-Umstellung** in allen 42 Dateien:
- `Link, NavLink, Navigate, Outlet, createBrowserRouter, useMatch,
useNavigate, useParams, useSearchParams` → `from 'react-router'`
- ⚠️ **`RouterProvider` → `from 'react-router/dom'`** (Sonderfall! NICHT
`react-router`). Betrifft nur `frontend/src/main.tsx:4`.

## Voraussetzungen / Blocker (WICHTIG)

- **React ≥ 19.2.7** — wir sind exakt bei `19.2.7` ✅ (peerDep von react-router@8.3.0
ist `react >=19.2.7`, `react-dom >=19.2.7`).
- **Node ≥ 22.22** — ⚠️ **BLOCKER:** `.github/workflows/ci.yml` `setup-node`
läuft aktuell auf **Node 20**. Muss im selben PR auf **≥ 22.22** (empf. 24) gehoben
werden, sonst bricht `vite build` unter der v8-Engines-Anforderung. Frontend-Dockerfile
nutzt bereits `node:26-alpine` ✅.
- **Vite ≥ 7** — wir sind bei `^8.1.4` ✅ (nur für Framework-Mode relevant, wir nicht).

## Aufgabenliste

1. **Worktree** anlegen (Wolfgangs Standard für Implementierungs-Sessions).
2. `frontend/package.json`: `react-router-dom` raus, `"react-router": "^8.3.0"` rein.
3. Import-Umstellung 42 Dateien (scripted sed für 9 Symbole → `react-router`;
`main.tsx` `RouterProvider` → `react-router/dom` manuell/separat).
4. `.github/workflows/ci.yml`: `node-version: '20'` → `'22'` (oder `'24'`).
5. `npm install` → `package-lock.json` aktualisieren; prüfen, dass `react-router-dom`
komplett aus dem Lockfile verschwindet.
6. Alle **drei** Interim-Ausnahmen für `GHSA-qwww-vcr4-c8h2` **entfernen** (CVE dann echt behoben):
`.trivyignore`-Zeile, `ALLOWLIST`-Eintrag in `.github/scripts/audit-gate.mjs`,
und Dependabot-Alert-Dismiss ist mit dem Upgrade automatisch gegenstandslos.
7. **Verifikation:**
- `npm run typecheck` grün (Frontend hat kein Test-Framework → Typecheck ist das Gate).
- `npm run build` grün.
- Manueller Browser-Smoke DE/EN: Login → Navigation → Board → Issue-Dialog
(`?issue=`) → Settings-Tabs → Deep-Link-Routen (`useParams`/`useSearchParams`).
- PR-CI: Trivy-Gate grün OHNE die trivyignore-Ausnahme.
8. **Dependabot ignore lockern:** falls in `.github/dependabot.yml` ein react-router-
Ignore ergänzt wurde, wieder entfernen (aktuell nur `typescript` semver-major ignored).
9. **Release:** In einer v1.0.x-Version ausliefern; Memory + CHANGELOG aktualisieren.

## Risiko / Hinweise

- Frontend hat **kein** Test-Framework (Typecheck + manuell) → sorgfältiger
manueller Smoke ist Pflicht.
- Major-Bump: trotz identischer API-Namen auf subtile Verhaltensänderungen bei
relativem Routing / `Navigate replace` / `useSearchParams`-Defaults achten.
- Reihenfolge im PR beachten: Node-Bump (Schritt 4) muss zusammen mit dem
Paket-Bump landen, sonst CI-Rotlauf (vgl. TS7-Bump-Lektion, PR #81).
Loading