feat(cra): CRA Compliance module Phase 1+2+3 (intake, scope, path, requirements, backlog, sbom, checks)
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>
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
'use client'
|
||||
|
||||
import React, { useEffect, useState, useCallback, use } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { ClassificationBadge } from '../../_components/ClassificationBadge'
|
||||
|
||||
interface CRAProject {
|
||||
id: string
|
||||
name: string
|
||||
cra_classification: string | null
|
||||
conformity_path: string | null
|
||||
status: string
|
||||
}
|
||||
|
||||
type PathId = 'self_assessment' | 'harmonized_standard' | 'eucc' | 'notified_body'
|
||||
|
||||
interface PathOption {
|
||||
id: PathId
|
||||
modul: string
|
||||
title: string
|
||||
short: string
|
||||
details: string[]
|
||||
}
|
||||
|
||||
const PATHS: PathOption[] = [
|
||||
{
|
||||
id: 'self_assessment',
|
||||
modul: 'Modul A',
|
||||
title: 'Self-Assessment',
|
||||
short: 'Konformitaetsbewertung durch interne Pruefung',
|
||||
details: [
|
||||
'Hersteller fuehrt Konformitaetsbewertung selbst durch',
|
||||
'Geringster externer Aufwand, schnelle Umsetzung',
|
||||
'Default fuer Standard-Produkte',
|
||||
'Technische Dokumentation + DoC bleibt Pflicht',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'harmonized_standard',
|
||||
modul: 'Modul B',
|
||||
title: 'Harmonized Standard',
|
||||
short: 'Konformitaetsvermutung durch harmonisierte Norm',
|
||||
details: [
|
||||
'Anwendung einer harmonisierten EU-Norm (z.B. DIN EN 40000-1-2 Entwurf)',
|
||||
'Konformitaetsvermutung gemaess EU-Recht',
|
||||
'Geringeres Audit-Risiko',
|
||||
'Empfohlen bei verfuegbarer harmonisierter Norm',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'eucc',
|
||||
modul: 'Modul H',
|
||||
title: 'EUCC Zertifizierung',
|
||||
short: 'European Cybersecurity Certification Scheme',
|
||||
details: [
|
||||
'ENISA-EUCC-Zertifizierung (Common Criteria-basiert)',
|
||||
'Hoechste Anerkennung in EU + Drittstaaten',
|
||||
'Hoher Aufwand, ITSEF-Pruefung erforderlich',
|
||||
'Pflicht bei einigen Important Class II-Produkten',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'notified_body',
|
||||
modul: 'Modul C',
|
||||
title: 'Notified Body Assessment',
|
||||
short: 'Drittprueforganisation pruefn die Konformitaet',
|
||||
details: [
|
||||
'Externe Bewertung durch akkreditierte Stelle',
|
||||
'PFLICHT fuer Critical-Produkte (Annex IV)',
|
||||
'Hoechste Auditierbarkeit + Vertrauen',
|
||||
'Laufzeit + Kosten am hoechsten',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const ALLOWED: Record<string, PathId[]> = {
|
||||
STANDARD: ['self_assessment', 'harmonized_standard', 'eucc', 'notified_body'],
|
||||
IMPORTANT_I: ['self_assessment', 'harmonized_standard', 'eucc', 'notified_body'],
|
||||
IMPORTANT_II: ['harmonized_standard', 'eucc', 'notified_body'],
|
||||
CRITICAL: ['notified_body'],
|
||||
}
|
||||
|
||||
const DEFAULT_FOR: Record<string, PathId> = {
|
||||
STANDARD: 'self_assessment',
|
||||
IMPORTANT_I: 'self_assessment',
|
||||
IMPORTANT_II: 'harmonized_standard',
|
||||
CRITICAL: 'notified_body',
|
||||
}
|
||||
|
||||
export default function PathSelectPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ projectId: string }>
|
||||
}) {
|
||||
const { projectId } = use(params)
|
||||
const router = useRouter()
|
||||
const [project, setProject] = useState<CRAProject | null>(null)
|
||||
const [selected, setSelected] = useState<PathId | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const tenant = '00000000-0000-0000-0000-000000000001'
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/sdk/v1/cra/projects/${projectId}`, {
|
||||
headers: { 'X-Tenant-ID': tenant },
|
||||
})
|
||||
if (!res.ok) throw new Error(await res.text())
|
||||
const p: CRAProject = await res.json()
|
||||
setProject(p)
|
||||
if (p.conformity_path) {
|
||||
setSelected(p.conformity_path as PathId)
|
||||
} else if (p.cra_classification && DEFAULT_FOR[p.cra_classification]) {
|
||||
setSelected(DEFAULT_FOR[p.cra_classification])
|
||||
}
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Fehler beim Laden')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [projectId])
|
||||
|
||||
useEffect(() => { load() }, [load])
|
||||
|
||||
const submit = async () => {
|
||||
if (!selected) return
|
||||
setSaving(true)
|
||||
setError('')
|
||||
try {
|
||||
const res = await fetch(`/api/sdk/v1/cra/projects/${projectId}/path-select`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': tenant },
|
||||
body: JSON.stringify({ conformity_path: selected }),
|
||||
})
|
||||
if (!res.ok) throw new Error(await res.text())
|
||||
router.push(`/sdk/cra/${projectId}`)
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Speichern fehlgeschlagen')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) return <div className="min-h-screen bg-gray-50 p-8"><p className="text-gray-500">Laedt...</p></div>
|
||||
if (!project) return null
|
||||
|
||||
if (!project.cra_classification) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-8">
|
||||
<div className="max-w-2xl mx-auto bg-yellow-50 border border-yellow-200 rounded-lg p-6">
|
||||
<p className="text-yellow-800">
|
||||
Bitte erst den Scope-Check ausfuehren.
|
||||
<a href={`/sdk/cra/${projectId}/scope`} className="ml-2 underline">→ Zum Scope-Check</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const allowedPaths = ALLOWED[project.cra_classification] || []
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 py-8">
|
||||
<div className="max-w-4xl 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">Konformitaetspfad waehlen</h1>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
Schritt 3 von 3 — basierend auf der Klassifikation siehst du die zulaessigen Pfade.
|
||||
</p>
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
<span className="text-sm text-gray-600">Klassifikation:</span>
|
||||
<ClassificationBadge value={project.cra_classification} size="md" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 bg-red-50 border border-red-200 rounded-lg p-3 text-sm text-red-700">{error}</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
{PATHS.map(path => {
|
||||
const allowed = allowedPaths.includes(path.id)
|
||||
const isSelected = selected === path.id
|
||||
return (
|
||||
<button
|
||||
key={path.id}
|
||||
onClick={() => allowed && setSelected(path.id)}
|
||||
disabled={!allowed}
|
||||
className={`text-left p-5 rounded-xl border-2 transition-all ${
|
||||
isSelected ? 'border-red-500 bg-red-50' :
|
||||
allowed ? 'border-gray-200 bg-white hover:border-red-300 hover:shadow-md' :
|
||||
'border-gray-200 bg-gray-50 opacity-50 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div>
|
||||
<span className="text-xs font-semibold text-gray-500 uppercase tracking-wide">{path.modul}</span>
|
||||
<h3 className="text-lg font-semibold text-gray-900">{path.title}</h3>
|
||||
</div>
|
||||
{isSelected && (
|
||||
<span className="px-2 py-0.5 text-xs bg-red-600 text-white rounded">Gewaehlt</span>
|
||||
)}
|
||||
{!allowed && (
|
||||
<span className="px-2 py-0.5 text-xs bg-gray-200 text-gray-600 rounded">Nicht zulaessig</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mb-3">{path.short}</p>
|
||||
<ul className="text-xs text-gray-600 space-y-1">
|
||||
{path.details.map((d, i) => (
|
||||
<li key={i} className="flex items-start gap-1.5">
|
||||
<span className="text-gray-400 mt-0.5">•</span>
|
||||
<span>{d}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-4 flex items-center justify-between">
|
||||
<div className="text-sm text-gray-600">
|
||||
{selected ? (
|
||||
<>Ausgewaehlt: <span className="font-medium text-gray-900">
|
||||
{PATHS.find(p => p.id === selected)?.title}
|
||||
</span></>
|
||||
) : (
|
||||
'Keine Auswahl getroffen'
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => router.push(`/sdk/cra/${projectId}/scope`)}
|
||||
disabled={saving}
|
||||
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50"
|
||||
>
|
||||
← Zurueck
|
||||
</button>
|
||||
<button
|
||||
onClick={submit}
|
||||
disabled={saving || !selected}
|
||||
className="px-6 py-2 bg-red-600 text-white font-medium rounded-lg hover:bg-red-700 disabled:bg-gray-300"
|
||||
>
|
||||
{saving ? 'Speichert...' : 'Pfad festlegen'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user