diff --git a/admin-compliance/app/api/sdk/v1/cra/projects/[id]/monitoring/route.ts b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/monitoring/route.ts new file mode 100644 index 00000000..46d56b33 --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/monitoring/route.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002' + +export async function GET(request: NextRequest, ctx: { params: Promise<{ id: string }> }) { + const { id } = await ctx.params + const tenantId = request.headers.get('x-tenant-id') || '00000000-0000-0000-0000-000000000001' + try { + const resp = await fetch(`${BACKEND_URL}/api/v1/cra/projects/${id}/monitoring`, { + headers: { 'X-Tenant-ID': tenantId }, + }) + const text = await resp.text() + return new NextResponse(text, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }) + } catch (err) { + return NextResponse.json({ error: 'Backend unreachable', details: String(err) }, { status: 502 }) + } +} diff --git a/admin-compliance/app/api/sdk/v1/cra/projects/[id]/vulnerabilities/route.ts b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/vulnerabilities/route.ts new file mode 100644 index 00000000..a7477b75 --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/vulnerabilities/route.ts @@ -0,0 +1,42 @@ +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002' + +function tenant(req: NextRequest) { + return req.headers.get('x-tenant-id') || '00000000-0000-0000-0000-000000000001' +} + +export async function GET(request: NextRequest, ctx: { params: Promise<{ id: string }> }) { + const { id } = await ctx.params + try { + const resp = await fetch(`${BACKEND_URL}/api/v1/cra/projects/${id}/vulnerabilities`, { + headers: { 'X-Tenant-ID': tenant(request) }, + }) + const text = await resp.text() + return new NextResponse(text, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }) + } catch (err) { + return NextResponse.json({ error: 'Backend unreachable', details: String(err) }, { status: 502 }) + } +} + +export async function POST(request: NextRequest, ctx: { params: Promise<{ id: string }> }) { + const { id } = await ctx.params + const body = await request.text() + try { + const resp = await fetch(`${BACKEND_URL}/api/v1/cra/projects/${id}/vulnerabilities`, { + method: 'POST', + headers: { 'X-Tenant-ID': tenant(request), 'Content-Type': 'application/json' }, + body, + }) + const text = await resp.text() + return new NextResponse(text, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }) + } catch (err) { + return NextResponse.json({ error: 'Backend unreachable', details: String(err) }, { status: 502 }) + } +} diff --git a/admin-compliance/app/api/sdk/v1/cra/vulnerabilities/[vulnId]/route.ts b/admin-compliance/app/api/sdk/v1/cra/vulnerabilities/[vulnId]/route.ts new file mode 100644 index 00000000..474331c7 --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/cra/vulnerabilities/[vulnId]/route.ts @@ -0,0 +1,43 @@ +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002' + +function tenant(req: NextRequest) { + return req.headers.get('x-tenant-id') || '00000000-0000-0000-0000-000000000001' +} + +export async function PATCH(request: NextRequest, ctx: { params: Promise<{ vulnId: string }> }) { + const { vulnId } = await ctx.params + const body = await request.text() + try { + const resp = await fetch(`${BACKEND_URL}/api/v1/cra/projects/vulnerabilities/${vulnId}`, { + method: 'PATCH', + headers: { 'X-Tenant-ID': tenant(request), 'Content-Type': 'application/json' }, + body, + }) + const text = await resp.text() + return new NextResponse(text, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }) + } catch (err) { + return NextResponse.json({ error: 'Backend unreachable', details: String(err) }, { status: 502 }) + } +} + +export async function DELETE(request: NextRequest, ctx: { params: Promise<{ vulnId: string }> }) { + const { vulnId } = await ctx.params + try { + const resp = await fetch(`${BACKEND_URL}/api/v1/cra/projects/vulnerabilities/${vulnId}`, { + method: 'DELETE', + headers: { 'X-Tenant-ID': tenant(request) }, + }) + const text = await resp.text() + return new NextResponse(text, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }) + } catch (err) { + return NextResponse.json({ error: 'Backend unreachable', details: String(err) }, { status: 502 }) + } +} diff --git a/admin-compliance/app/sdk/cra/[projectId]/monitoring/page.tsx b/admin-compliance/app/sdk/cra/[projectId]/monitoring/page.tsx new file mode 100644 index 00000000..f8ef7737 --- /dev/null +++ b/admin-compliance/app/sdk/cra/[projectId]/monitoring/page.tsx @@ -0,0 +1,168 @@ +'use client' + +import React, { useEffect, useState, useCallback, use } from 'react' + +interface MonitoringData { + project_id: string + deadlines: { date: string; label: string }[] + summary: { + active_vulns: number + critical_vulns: number + high_vulns: number + breached_24h_reporting: number + breached_72h_reporting: number + sbom_versions: number + configured_checks: number + } + post_market_checklist: { item: string; done: boolean; href_suffix: string }[] +} + +export default function MonitoringPage({ + params, +}: { + params: Promise<{ projectId: string }> +}) { + const { projectId } = use(params) + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState('') + + const load = useCallback(async () => { + try { + const res = await fetch(`/api/sdk/v1/cra/projects/${projectId}/monitoring`, { + headers: { 'X-Tenant-ID': '00000000-0000-0000-0000-000000000001' }, + }) + if (!res.ok) throw new Error(await res.text()) + setData(await res.json()) + } catch (e) { + setError(e instanceof Error ? e.message : 'Fehler beim Laden') + } finally { + setLoading(false) + } + }, [projectId]) + + useEffect(() => { load() }, [load]) + + if (loading) return

Laedt...

+ if (error) return

{error}

+ if (!data) return null + + const completeness = data.post_market_checklist.filter(c => c.done).length + const totalChecks = data.post_market_checklist.length + + return ( +
+
+
+ + ← Zurueck zum Projekt + +

Post-Market Monitoring

+

+ CRA-Stichtage + Vuln-Reporting-Compliance + Post-Market-Pflichten. +

+
+ + {/* CRA-Stichtage */} +
+

CRA-Stichtage

+
+ {data.deadlines.map(d => { + const target = new Date(d.date).getTime() + const days = Math.round((target - Date.now()) / 86400000) + const isPast = days < 0 + const isSoon = days >= 0 && days < 90 + const styles = isPast ? 'bg-gray-100 border-gray-200' : + isSoon ? 'bg-red-50 border-red-200' : + days < 365 ? 'bg-orange-50 border-orange-200' : + 'bg-blue-50 border-blue-200' + return ( +
+
{d.date}
+
{d.label}
+
+ {isPast ? `vor ${-days} Tagen` : `noch ${days} Tage`} +
+
+ ) + })} +
+
+ + {/* Vuln-Reporting Compliance Banner */} + {(data.summary.breached_24h_reporting > 0 || data.summary.breached_72h_reporting > 0) && ( +
+

⚠ CRA-Pflichten verletzt

+ {data.summary.breached_24h_reporting > 0 && ( +

+ {data.summary.breached_24h_reporting} Schwachstelle(n) ohne 24h-Fruehwarnung an ENISA — Art. 14(2)(a) CRA. +

+ )} + {data.summary.breached_72h_reporting > 0 && ( +

+ {data.summary.breached_72h_reporting} Schwachstelle(n) ohne 72h-Detailbericht — Art. 14(2)(b) CRA. +

+ )} + + Zu den Schwachstellen → + +
+ )} + + {/* Summary Cards */} +
+ + 0 ? 'green' : 'gray'} /> + 0 ? 'green' : 'gray'} /> + +
+ + {/* Post-Market Checklist */} +
+

Post-Market-Pflichten

+
    + {data.post_market_checklist.map((c, i) => ( +
  • + + {c.done ? '✓' : '○'} + + {c.item} + {!c.done && ( + + Erledigen → + + )} +
  • + ))} +
+
+ +
+ Hinweis: Diese Seite aggregiert CRA-Pflichten aus SBOM, Checks und Vulnerability-Tracker. Die Reporting-Pflichten 24h/72h gelten ab CRA Art. 14(2) — verletzte Fristen erscheinen als rotes Banner. +
+
+
+ ) +} + +function SummaryCard({ label, value, subtitle, color }: { label: string; value: number | string; subtitle: string; color: 'blue' | 'red' | 'green' | 'orange' | 'gray' }) { + const bg = { + blue: 'bg-blue-50 border-blue-200 text-blue-700', + red: 'bg-red-50 border-red-200 text-red-700', + green: 'bg-green-50 border-green-200 text-green-700', + orange: 'bg-orange-50 border-orange-200 text-orange-700', + gray: 'bg-gray-50 border-gray-200 text-gray-600', + }[color] + return ( +
+

{label}

+

{value}

+

{subtitle}

+
+ ) +} diff --git a/admin-compliance/app/sdk/cra/[projectId]/page.tsx b/admin-compliance/app/sdk/cra/[projectId]/page.tsx index ac11cfa7..233fc447 100644 --- a/admin-compliance/app/sdk/cra/[projectId]/page.tsx +++ b/admin-compliance/app/sdk/cra/[projectId]/page.tsx @@ -175,31 +175,14 @@ export default function CRAProjectDashboard({ )} -
- - → Requirements (40) - - - → Backlog - - - → SBOM - - - → Checks - +
+ Requirements + Backlog + SBOM + Checks + Vulns (CVD) + Monitoring + Dokumente
diff --git a/admin-compliance/app/sdk/cra/[projectId]/vuln/page.tsx b/admin-compliance/app/sdk/cra/[projectId]/vuln/page.tsx new file mode 100644 index 00000000..e1fbc918 --- /dev/null +++ b/admin-compliance/app/sdk/cra/[projectId]/vuln/page.tsx @@ -0,0 +1,385 @@ +'use client' + +import React, { useEffect, useState, useCallback, use } from 'react' +import { SeverityBadge } from '../../_components/SeverityBadge' + +interface Vuln { + id: string + cve_id: string | null + title: string + description: string + severity: string | null + cvss_score: number | null + affected_components: string[] + reporter_source: string + reporter_contact: string | null + discovered_at: string + triaged_at: string | null + patched_at: string | null + disclosed_at: string | null + embargo_until: string | null + reported_to_enisa_at: string | null + detailed_report_at: string | null + status: string + notes: string +} + +interface VulnListResponse { + project_id: string + total: number + summary: { + critical_open: number + breached_24h_reporting: number + breached_72h_reporting: number + by_status: Record + } + items: Vuln[] +} + +const STATUS_LABEL: Record = { + reported: 'Gemeldet', + triaged: 'Triagiert', + patched: 'Gepatcht', + disclosed: 'Offengelegt', + withdrawn: 'Zurueckgezogen', +} + +const STATUS_NEXT: Record = { + reported: { status: 'triaged', label: 'Triagieren' }, + triaged: { status: 'patched', label: 'Patch verfuegbar' }, + patched: { status: 'disclosed', label: 'Offenlegen' }, + disclosed: null, + withdrawn: null, +} + +function ageHours(iso: string | null): number { + if (!iso) return 0 + return (Date.now() - new Date(iso).getTime()) / 3600000 +} + +function fmtRemaining(iso: string | null, hours: number): { label: string; color: string } { + if (!iso) return { label: '—', color: 'text-gray-400' } + const age = ageHours(iso) + const remaining = hours - age + if (remaining < 0) return { label: `+${Math.round(-remaining)}h ueber Frist`, color: 'text-red-600 font-semibold' } + if (remaining < 4) return { label: `noch ${remaining.toFixed(1)}h`, color: 'text-orange-600 font-semibold' } + return { label: `noch ${Math.round(remaining)}h`, color: 'text-gray-600' } +} + +export default function VulnPage({ + params, +}: { + params: Promise<{ projectId: string }> +}) { + const { projectId } = use(params) + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + const [showForm, setShowForm] = useState(false) + const [error, setError] = useState('') + const [creating, setCreating] = useState(false) + const [transitioning, setTransitioning] = useState(null) + + // New vuln form state + const [title, setTitle] = useState('') + const [cveId, setCveId] = useState('') + const [severity, setSeverity] = useState('') + const [cvssScore, setCvssScore] = useState('') + const [description, setDescription] = useState('') + const [components, setComponents] = useState('') + const [reporterSource, setReporterSource] = useState('internal') + const [reporterContact, setReporterContact] = useState('') + + const tenant = '00000000-0000-0000-0000-000000000001' + + const load = useCallback(async () => { + try { + const res = await fetch(`/api/sdk/v1/cra/projects/${projectId}/vulnerabilities`, { + headers: { 'X-Tenant-ID': tenant }, + }) + if (!res.ok) throw new Error(await res.text()) + setData(await res.json()) + } catch (e) { + setError(e instanceof Error ? e.message : 'Fehler beim Laden') + } finally { + setLoading(false) + } + }, [projectId]) + + useEffect(() => { load() }, [load]) + + const create = async () => { + if (!title.trim()) return + setCreating(true) + setError('') + try { + const res = await fetch(`/api/sdk/v1/cra/projects/${projectId}/vulnerabilities`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': tenant }, + body: JSON.stringify({ + title, + cve_id: cveId || null, + description, + severity: severity || null, + cvss_score: cvssScore ? parseFloat(cvssScore) : null, + affected_components: components.split(',').map(s => s.trim()).filter(Boolean), + reporter_source: reporterSource, + reporter_contact: reporterContact || null, + }), + }) + if (!res.ok) throw new Error(await res.text()) + setShowForm(false) + setTitle(''); setCveId(''); setSeverity(''); setCvssScore('') + setDescription(''); setComponents(''); setReporterContact('') + await load() + } catch (e) { + setError(e instanceof Error ? e.message : 'Anlegen fehlgeschlagen') + } finally { + setCreating(false) + } + } + + const transition = async (vulnId: string, nextStatus: string) => { + setTransitioning(vulnId) + setError('') + try { + const res = await fetch(`/api/sdk/v1/cra/vulnerabilities/${vulnId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': tenant }, + body: JSON.stringify({ status: nextStatus }), + }) + if (!res.ok) throw new Error(await res.text()) + await load() + } catch (e) { + setError(e instanceof Error ? e.message : 'Statuswechsel fehlgeschlagen') + } finally { + setTransitioning(null) + } + } + + const markReported = async (vulnId: string, field: 'reported_to_enisa_at' | 'detailed_report_at') => { + setTransitioning(vulnId) + setError('') + try { + const res = await fetch(`/api/sdk/v1/cra/vulnerabilities/${vulnId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': tenant }, + body: JSON.stringify({ [field]: new Date().toISOString() }), + }) + if (!res.ok) throw new Error(await res.text()) + await load() + } catch (e) { + setError(e instanceof Error ? e.message : 'Reporting fehlgeschlagen') + } finally { + setTransitioning(null) + } + } + + if (loading) return

Laedt...

+ + return ( +
+
+
+ + ← Zurueck zum Projekt + +

Vulnerability Disclosure (CVD)

+

+ Schwachstellen tracken. CRA-Pflichten: 24h Fruehwarnung an ENISA, 72h Detailbericht. +

+
+ + {error && ( +
+
{error}
+ +
+ )} + + {/* Summary KPIs */} + {data && ( +
+ + 0 ? 'red' : 'green'} /> + 0 ? 'red' : 'green'} /> + 0 ? 'red' : 'green'} /> +
+ )} + + + + {showForm && ( +
+

Neue Schwachstelle

+
+
+ + setTitle(e.target.value)} className="w-full px-3 py-2 border rounded text-sm" /> +
+
+ + setCveId(e.target.value)} placeholder="CVE-2026-12345" className="w-full px-3 py-2 border rounded text-sm font-mono" /> +
+
+ + +
+
+ + setCvssScore(e.target.value)} className="w-full px-3 py-2 border rounded text-sm" /> +
+
+ + +
+
+ + setReporterContact(e.target.value)} placeholder="email@..." className="w-full px-3 py-2 border rounded text-sm" /> +
+
+ + setComponents(e.target.value)} placeholder="lodash@4.17.20, axios@0.21.0" className="w-full px-3 py-2 border rounded text-sm font-mono" /> +
+
+ +