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>
173 lines
7.0 KiB
TypeScript
173 lines
7.0 KiB
TypeScript
'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
|
|
intended_use: string
|
|
primary_language: string | null
|
|
connected_to_internet: boolean
|
|
has_software_updates: boolean
|
|
processes_personal_data: boolean
|
|
is_critical_infra_supplier: boolean
|
|
cra_classification: string | null
|
|
classification_rationale: string[]
|
|
status: string
|
|
}
|
|
|
|
const CLASSIFICATION_DESC: Record<string, string> = {
|
|
NOT_IN_SCOPE: 'Dein Produkt enthaelt keine digitalen Elemente nach CRA-Definition. Es ist nicht vom CRA betroffen.',
|
|
STANDARD: 'Default-Kategorie fuer Produkte mit digitalen Elementen. Self-Assessment (Modul A) ist der typische Pfad.',
|
|
IMPORTANT_I: 'Annex III Klasse I — Wichtige Produkte mit erhoehten Anforderungen. Self-Assessment OR Harmonized Standard moeglich.',
|
|
IMPORTANT_II: 'Annex III Klasse II — Wichtige Produkte mit hohem Sicherheitsbedarf. Harmonized Standard ODER EUCC ODER Notified Body.',
|
|
CRITICAL: 'Annex IV — Kritische Produkte (z.B. HSM, Smart-Meter-Gateways). Notified-Body-Assessment Pflicht.',
|
|
}
|
|
|
|
export default function ScopeCheckPage({
|
|
params,
|
|
}: {
|
|
params: Promise<{ projectId: string }>
|
|
}) {
|
|
const { projectId } = use(params)
|
|
const router = useRouter()
|
|
const [project, setProject] = useState<CRAProject | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [checking, setChecking] = 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())
|
|
setProject(await res.json())
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : 'Fehler beim Laden')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, [projectId])
|
|
|
|
useEffect(() => { load() }, [load])
|
|
|
|
const runScopeCheck = async () => {
|
|
setChecking(true)
|
|
setError('')
|
|
try {
|
|
const res = await fetch(`/api/sdk/v1/cra/projects/${projectId}/scope-check`, {
|
|
method: 'POST',
|
|
headers: { 'X-Tenant-ID': tenant },
|
|
})
|
|
if (!res.ok) throw new Error(await res.text())
|
|
setProject(await res.json())
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : 'Klassifikation fehlgeschlagen')
|
|
} finally {
|
|
setChecking(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
|
|
|
|
const hasResult = !!project.cra_classification
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 py-8">
|
|
<div className="max-w-3xl 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">Scope-Check & Klassifikation</h1>
|
|
<p className="text-sm text-gray-600 mt-1">
|
|
Schritt 2 von 3 — Wir matchen dein Intake gegen Annex III/IV des CRA.
|
|
</p>
|
|
</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="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-4">
|
|
<h3 className="text-sm font-semibold text-gray-700">Aktuelle Intake-Daten</h3>
|
|
<dl className="grid grid-cols-1 md:grid-cols-2 gap-2 text-sm">
|
|
<Field label="Produkt" value={project.name} />
|
|
<Field label="Sprache" value={project.primary_language || '—'} />
|
|
<Field label="Intended Use" value={project.intended_use || '—'} fullWidth />
|
|
<Field label="Internet" value={project.connected_to_internet ? 'Ja' : 'Nein'} />
|
|
<Field label="Software-Updates" value={project.has_software_updates ? 'Ja' : 'Nein'} />
|
|
<Field label="Personenbezogene Daten" value={project.processes_personal_data ? 'Ja' : 'Nein'} />
|
|
<Field label="Kritische Infra" value={project.is_critical_infra_supplier ? 'Ja' : 'Nein'} />
|
|
</dl>
|
|
|
|
<div className="border-t border-gray-200 pt-4">
|
|
<button
|
|
onClick={runScopeCheck}
|
|
disabled={checking}
|
|
className="w-full py-3 bg-red-600 text-white font-medium rounded-lg hover:bg-red-700 disabled:bg-gray-300"
|
|
>
|
|
{checking ? 'Pruefe...' : hasResult ? 'Klassifikation neu berechnen' : 'Klassifikation berechnen'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{hasResult && (
|
|
<div className="mt-6 bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
|
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wide mb-3">Ergebnis</h3>
|
|
<div className="flex items-center gap-4 mb-4">
|
|
<ClassificationBadge value={project.cra_classification} size="lg" />
|
|
<p className="text-sm text-gray-700">
|
|
{CLASSIFICATION_DESC[project.cra_classification!]}
|
|
</p>
|
|
</div>
|
|
{project.classification_rationale?.length > 0 && (
|
|
<div className="bg-gray-50 rounded-lg p-4 mb-4">
|
|
<p className="text-xs font-semibold text-gray-600 uppercase tracking-wide mb-2">Begruendung</p>
|
|
<ul className="list-disc list-inside space-y-1 text-sm text-gray-700">
|
|
{project.classification_rationale.map((r, i) => <li key={i}>{r}</li>)}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
<div className="flex gap-3">
|
|
<button
|
|
onClick={() => router.push(`/sdk/cra/${projectId}/intake`)}
|
|
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50"
|
|
>
|
|
← Intake anpassen
|
|
</button>
|
|
<button
|
|
onClick={() => router.push(`/sdk/cra/${projectId}/path`)}
|
|
disabled={project.cra_classification === 'NOT_IN_SCOPE'}
|
|
className="flex-1 py-2 bg-red-600 text-white font-medium rounded-lg hover:bg-red-700 disabled:bg-gray-300"
|
|
>
|
|
Weiter zum Konformitaetspfad →
|
|
</button>
|
|
</div>
|
|
{project.cra_classification === 'NOT_IN_SCOPE' && (
|
|
<p className="text-xs text-gray-500 mt-2 text-center">
|
|
Produkt ist nicht im CRA-Scope. Keine weiteren Schritte noetig.
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function Field({ label, value, fullWidth }: { label: string; value: string; fullWidth?: boolean }) {
|
|
return (
|
|
<div className={fullWidth ? 'md:col-span-2' : ''}>
|
|
<dt className="text-xs text-gray-500">{label}</dt>
|
|
<dd className="text-gray-900 mt-0.5">{value}</dd>
|
|
</div>
|
|
)
|
|
}
|