'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" />