1cf5de1d45
Phase 1 — Intake + Scope + Path: - Migration 119: compliance_cra_projects table (intake + classification + path + status state machine) - Backend service cra_routes.py: CRUD + scope-check + path-select - Deterministic Annex III/IV classifier (verbatim mapping from migration 059 wiki) - Path validation per classification (CRITICAL → notified_body mandatory) - Frontend: project list, dashboard, 3-step wizard (intake/scope/path) - Sidebar entry under "CRA Compliance" (red) Phase 2 — Annex I Requirements + Priorisierungs-Backlog: - cra_annex_i_data.py: 40 Annex-I requirements (8 categories), 9 measures (M540-M548), 3 CRA deadlines - Endpoints: /requirements (40 items), /backlog (priority-sorted with deadline pressure) - Frontend: requirements table with filters + expandable details, backlog with deadline banner + score-ranked table - Dashboard KPI cards (Critical count, days to CE deadline, etc.) + top-10 backlog snippet Phase 3 — SBOM Upload + Automated Checks: - Migration 120: compliance_cra_sboms (versioned uploads, CycloneDX + SPDX) - SBOM endpoints: POST /sbom/upload (format detection, summary extraction), GET /sboms - Checks reuse compliance_evidence_checks: init creates 6 default CRA checks, run executes - Real implementations: cra_security_txt (HTTP + Contact: line) and cra_tls_cert_check (TLS handshake) - Frontend: SBOM file upload + version list, Checks page with per-check URL input + Run button Backend-Reuse: gap_projects (intake pre-population), compliance_evidence_checks/_check_results. Tenant scoping via existing X-Tenant-ID header pattern. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
183 lines
7.9 KiB
TypeScript
183 lines
7.9 KiB
TypeScript
'use client'
|
|
|
|
import React, { useEffect, useState, useCallback, use } from 'react'
|
|
import { SeverityBadge } from '../../_components/SeverityBadge'
|
|
|
|
interface Requirement {
|
|
req_id: string
|
|
n: number
|
|
category: string
|
|
title: string
|
|
annex_anchor: string
|
|
iso27001_ref: string[]
|
|
description: string
|
|
severity: string
|
|
mapped_measures: string[]
|
|
mapped_measure_names: { id: string; name: string }[]
|
|
evidence_type: string
|
|
effort_days: number
|
|
status: string
|
|
}
|
|
|
|
interface RequirementsResponse {
|
|
project_id: string
|
|
classification: string | null
|
|
total: number
|
|
items: Requirement[]
|
|
}
|
|
|
|
export default function RequirementsPage({
|
|
params,
|
|
}: {
|
|
params: Promise<{ projectId: string }>
|
|
}) {
|
|
const { projectId } = use(params)
|
|
const [data, setData] = useState<RequirementsResponse | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState('')
|
|
const [filterCategory, setFilterCategory] = useState<string>('all')
|
|
const [filterSeverity, setFilterSeverity] = useState<string>('all')
|
|
const [expanded, setExpanded] = useState<string | null>(null)
|
|
|
|
const load = useCallback(async () => {
|
|
try {
|
|
const res = await fetch(`/api/sdk/v1/cra/projects/${projectId}/requirements`, {
|
|
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 categories = Array.from(new Set(data.items.map(i => i.category)))
|
|
const filtered = data.items.filter(r =>
|
|
(filterCategory === 'all' || r.category === filterCategory) &&
|
|
(filterSeverity === 'all' || r.severity === filterSeverity)
|
|
)
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 py-8">
|
|
<div className="max-w-7xl 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">CRA Annex I Requirements</h1>
|
|
<p className="text-sm text-gray-600 mt-1">
|
|
Alle {data.total} Essential Cybersecurity Requirements aus Annex I. Status bleibt "unbewertet" bis Evidence-Checks in Phase 3 verknuepft sind.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex gap-3 mb-4 flex-wrap">
|
|
<select
|
|
value={filterCategory}
|
|
onChange={e => setFilterCategory(e.target.value)}
|
|
className="px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
|
>
|
|
<option value="all">Alle Kategorien</option>
|
|
{categories.map(c => <option key={c} value={c}>{c}</option>)}
|
|
</select>
|
|
<select
|
|
value={filterSeverity}
|
|
onChange={e => setFilterSeverity(e.target.value)}
|
|
className="px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
|
>
|
|
<option value="all">Alle Severities</option>
|
|
<option value="CRITICAL">Kritisch</option>
|
|
<option value="HIGH">Hoch</option>
|
|
<option value="MEDIUM">Mittel</option>
|
|
<option value="LOW">Niedrig</option>
|
|
</select>
|
|
<span className="text-sm text-gray-500 self-center">
|
|
{filtered.length} von {data.total}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
|
<table className="w-full">
|
|
<thead className="bg-gray-50 border-b border-gray-200">
|
|
<tr>
|
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">#</th>
|
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Anforderung</th>
|
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Kategorie</th>
|
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Severity</th>
|
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Aufwand</th>
|
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-gray-100">
|
|
{filtered.map(req => (
|
|
<React.Fragment key={req.req_id}>
|
|
<tr
|
|
className="hover:bg-gray-50 cursor-pointer"
|
|
onClick={() => setExpanded(expanded === req.req_id ? null : req.req_id)}
|
|
>
|
|
<td className="px-3 py-2 text-sm text-gray-500">{req.n}</td>
|
|
<td className="px-3 py-2">
|
|
<div className="text-sm font-medium text-gray-900">{req.title}</div>
|
|
<div className="text-xs text-gray-500">{req.annex_anchor} · {req.req_id}</div>
|
|
</td>
|
|
<td className="px-3 py-2 text-sm text-gray-600">{req.category}</td>
|
|
<td className="px-3 py-2"><SeverityBadge value={req.severity} /></td>
|
|
<td className="px-3 py-2 text-sm text-gray-600">{req.effort_days} PT</td>
|
|
<td className="px-3 py-2">
|
|
<span className="px-2 py-0.5 text-xs rounded-full bg-gray-100 text-gray-600">{req.status}</span>
|
|
</td>
|
|
</tr>
|
|
{expanded === req.req_id && (
|
|
<tr>
|
|
<td colSpan={6} className="px-4 py-4 bg-blue-50">
|
|
<div className="space-y-3">
|
|
<div>
|
|
<p className="text-xs font-semibold text-gray-600 uppercase">Beschreibung</p>
|
|
<p className="text-sm text-gray-700 mt-1">{req.description}</p>
|
|
</div>
|
|
{req.iso27001_ref.length > 0 && (
|
|
<div>
|
|
<p className="text-xs font-semibold text-gray-600 uppercase">ISO 27001:2022 Mapping</p>
|
|
<p className="text-sm text-gray-700 mt-1">
|
|
{req.iso27001_ref.map(r => (
|
|
<span key={r} className="inline-block mr-2 mb-1 px-2 py-0.5 bg-white rounded text-xs">{r}</span>
|
|
))}
|
|
</p>
|
|
</div>
|
|
)}
|
|
{req.mapped_measure_names.length > 0 && (
|
|
<div>
|
|
<p className="text-xs font-semibold text-gray-600 uppercase">Empfohlene Massnahmen</p>
|
|
<ul className="text-sm text-gray-700 mt-1 space-y-0.5">
|
|
{req.mapped_measure_names.map(m => (
|
|
<li key={m.id}>
|
|
<span className="font-mono text-xs text-gray-500">{m.id}</span> — {m.name}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
<div className="text-xs text-gray-500 pt-1">
|
|
Evidence-Typ: <span className="font-medium">{req.evidence_type}</span>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</React.Fragment>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|