cc80e59e5e
Migration 121: compliance_cra_vulnerabilities table with full lifecycle tracking
- Status state machine: reported → triaged → patched → disclosed (+ withdrawn)
- CRA Art. 14(2) deadlines tracked: reported_to_enisa_at (24h), detailed_report_at (72h)
- CVE-ID, severity, CVSS, affected_components (JSONB), embargo_until
Backend endpoints in cra_routes.py:
- POST /vulnerabilities — create with validation (severity, CVSS range)
- GET /vulnerabilities — list with deadline-breach summary (24h/72h counters)
- PATCH /vulnerabilities/{id} — update fields + auto-set lifecycle timestamps
- DELETE /vulnerabilities/{id} — soft-delete (withdrawn)
- GET /monitoring — combined view: CRA deadlines + vuln summary + post-market checklist
Frontend:
- /vuln page: intake form, vuln cards with 24h/72h-countdown buttons,
status-transition flow with auto-timestamps
- /monitoring page: CRA deadlines (11.06.26 / 11.09.26 / 11.12.27), breach banner
if 24h/72h obligations missed, post-market checklist with deep-links
- Dashboard: +2 buttons (Vulns, Monitoring)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
169 lines
7.7 KiB
TypeScript
169 lines
7.7 KiB
TypeScript
'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<MonitoringData | null>(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 <div className="min-h-screen bg-gray-50 p-8"><p className="text-gray-500">Laedt...</p></div>
|
|
if (error) return <div className="min-h-screen bg-gray-50 p-8"><p className="text-red-600">{error}</p></div>
|
|
if (!data) return null
|
|
|
|
const completeness = data.post_market_checklist.filter(c => c.done).length
|
|
const totalChecks = data.post_market_checklist.length
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 py-8">
|
|
<div className="max-w-5xl mx-auto px-4">
|
|
<div className="mb-6">
|
|
<a href={`/sdk/cra/${projectId}`} className="text-sm text-blue-600 hover:underline">
|
|
← Zurueck zum Projekt
|
|
</a>
|
|
<h1 className="text-2xl font-bold text-gray-900 mt-2">Post-Market Monitoring</h1>
|
|
<p className="text-sm text-gray-600 mt-1">
|
|
CRA-Stichtage + Vuln-Reporting-Compliance + Post-Market-Pflichten.
|
|
</p>
|
|
</div>
|
|
|
|
{/* CRA-Stichtage */}
|
|
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-5 mb-6">
|
|
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wide mb-3">CRA-Stichtage</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
{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 (
|
|
<div key={d.date} className={`rounded-lg border p-4 ${styles}`}>
|
|
<div className="text-xs text-gray-500">{d.date}</div>
|
|
<div className="font-semibold text-gray-900 text-sm mt-0.5">{d.label}</div>
|
|
<div className="text-xs mt-1 text-gray-700">
|
|
{isPast ? `vor ${-days} Tagen` : `noch ${days} Tage`}
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Vuln-Reporting Compliance Banner */}
|
|
{(data.summary.breached_24h_reporting > 0 || data.summary.breached_72h_reporting > 0) && (
|
|
<div className="bg-red-50 border-2 border-red-300 rounded-xl p-5 mb-6">
|
|
<h3 className="text-sm font-bold text-red-900 uppercase tracking-wide mb-2">⚠ CRA-Pflichten verletzt</h3>
|
|
{data.summary.breached_24h_reporting > 0 && (
|
|
<p className="text-sm text-red-800">
|
|
<span className="font-semibold">{data.summary.breached_24h_reporting}</span> Schwachstelle(n) ohne 24h-Fruehwarnung an ENISA — Art. 14(2)(a) CRA.
|
|
</p>
|
|
)}
|
|
{data.summary.breached_72h_reporting > 0 && (
|
|
<p className="text-sm text-red-800 mt-1">
|
|
<span className="font-semibold">{data.summary.breached_72h_reporting}</span> Schwachstelle(n) ohne 72h-Detailbericht — Art. 14(2)(b) CRA.
|
|
</p>
|
|
)}
|
|
<a href={`/sdk/cra/${projectId}/vuln`} className="inline-block mt-2 text-sm text-red-700 underline font-medium">
|
|
Zu den Schwachstellen →
|
|
</a>
|
|
</div>
|
|
)}
|
|
|
|
{/* Summary Cards */}
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-6">
|
|
<SummaryCard label="Aktive Vulns" value={data.summary.active_vulns} subtitle={`${data.summary.critical_vulns} Critical · ${data.summary.high_vulns} High`} color="blue" />
|
|
<SummaryCard label="SBOM-Versionen" value={data.summary.sbom_versions} subtitle={data.summary.sbom_versions === 0 ? 'noch keine' : 'hochgeladen'} color={data.summary.sbom_versions > 0 ? 'green' : 'gray'} />
|
|
<SummaryCard label="Aktive Checks" value={data.summary.configured_checks} subtitle={data.summary.configured_checks === 0 ? 'init noetig' : 'konfiguriert'} color={data.summary.configured_checks > 0 ? 'green' : 'gray'} />
|
|
<SummaryCard label="Post-Market" value={`${completeness}/${totalChecks}`} subtitle="erfuellt" color={completeness === totalChecks ? 'green' : 'orange'} />
|
|
</div>
|
|
|
|
{/* Post-Market Checklist */}
|
|
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-5 mb-6">
|
|
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wide mb-3">Post-Market-Pflichten</h3>
|
|
<ul className="space-y-2">
|
|
{data.post_market_checklist.map((c, i) => (
|
|
<li key={i} className="flex items-center gap-3">
|
|
<span className={`w-5 h-5 rounded-full flex items-center justify-center text-xs flex-shrink-0 ${
|
|
c.done ? 'bg-green-500 text-white' : 'bg-gray-200 text-gray-400'
|
|
}`}>
|
|
{c.done ? '✓' : '○'}
|
|
</span>
|
|
<span className={`text-sm ${c.done ? 'text-gray-700' : 'text-gray-900 font-medium'}`}>{c.item}</span>
|
|
{!c.done && (
|
|
<a
|
|
href={`/sdk/cra/${projectId}/${c.href_suffix}`}
|
|
className="ml-auto text-xs text-blue-600 hover:underline"
|
|
>
|
|
Erledigen →
|
|
</a>
|
|
)}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
|
|
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4 text-sm text-blue-900">
|
|
<strong>Hinweis:</strong> 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.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
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 (
|
|
<div className={`rounded-xl border p-3 ${bg}`}>
|
|
<p className="text-xs uppercase tracking-wide">{label}</p>
|
|
<p className="text-2xl font-bold mt-1">{value}</p>
|
|
<p className="text-xs mt-0.5 opacity-80">{subtitle}</p>
|
|
</div>
|
|
)
|
|
}
|