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,240 @@
|
||||
'use client'
|
||||
|
||||
import React, { useEffect, useState, useCallback, use } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
const LANGUAGES = [
|
||||
{ value: '', label: '— bitte waehlen —' },
|
||||
{ value: 'js', label: 'JavaScript / TypeScript' },
|
||||
{ value: 'python', label: 'Python' },
|
||||
{ value: 'go', label: 'Go' },
|
||||
{ value: 'rust', label: 'Rust' },
|
||||
{ value: 'java', label: 'Java / Kotlin' },
|
||||
{ value: 'csharp', label: 'C# / .NET' },
|
||||
{ value: 'cpp', label: 'C / C++' },
|
||||
{ value: 'swift', label: 'Swift' },
|
||||
{ value: 'mixed', label: 'Mehrere Sprachen' },
|
||||
{ value: 'other', label: 'Andere' },
|
||||
]
|
||||
|
||||
interface CRAProject {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
repo_url: string | null
|
||||
primary_language: string | null
|
||||
has_firmware: boolean
|
||||
connected_to_internet: boolean
|
||||
has_software_updates: boolean
|
||||
processes_personal_data: boolean
|
||||
is_critical_infra_supplier: boolean
|
||||
intended_use: string
|
||||
}
|
||||
|
||||
export default function IntakePage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ projectId: string }>
|
||||
}) {
|
||||
const { projectId } = use(params)
|
||||
const router = useRouter()
|
||||
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const [name, setName] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
const [repoUrl, setRepoUrl] = useState('')
|
||||
const [primaryLanguage, setPrimaryLanguage] = useState('')
|
||||
const [hasFirmware, setHasFirmware] = useState(false)
|
||||
const [connectedInternet, setConnectedInternet] = useState(false)
|
||||
const [hasUpdates, setHasUpdates] = useState(false)
|
||||
const [processesPersonal, setProcessesPersonal] = useState(false)
|
||||
const [isCriticalInfra, setIsCriticalInfra] = useState(false)
|
||||
const [intendedUse, setIntendedUse] = 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()
|
||||
setName(p.name)
|
||||
setDescription(p.description || '')
|
||||
setRepoUrl(p.repo_url || '')
|
||||
setPrimaryLanguage(p.primary_language || '')
|
||||
setHasFirmware(p.has_firmware)
|
||||
setConnectedInternet(p.connected_to_internet)
|
||||
setHasUpdates(p.has_software_updates)
|
||||
setProcessesPersonal(p.processes_personal_data)
|
||||
setIsCriticalInfra(p.is_critical_infra_supplier)
|
||||
setIntendedUse(p.intended_use || '')
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Fehler beim Laden')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [projectId])
|
||||
|
||||
useEffect(() => { load() }, [load])
|
||||
|
||||
const save = async () => {
|
||||
setSaving(true)
|
||||
setError('')
|
||||
try {
|
||||
const res = await fetch(`/api/sdk/v1/cra/projects/${projectId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': tenant },
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
description,
|
||||
repo_url: repoUrl || null,
|
||||
primary_language: primaryLanguage || null,
|
||||
has_firmware: hasFirmware,
|
||||
connected_to_internet: connectedInternet,
|
||||
has_software_updates: hasUpdates,
|
||||
processes_personal_data: processesPersonal,
|
||||
is_critical_infra_supplier: isCriticalInfra,
|
||||
intended_use: intendedUse,
|
||||
status: 'scoped',
|
||||
}),
|
||||
})
|
||||
if (!res.ok) throw new Error(await res.text())
|
||||
router.push(`/sdk/cra/${projectId}/scope`)
|
||||
} 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>
|
||||
|
||||
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">Intake — Software-Profil</h1>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
Schritt 1 von 3 — Beschreibe Software, Firmware und Connectivity. Daraus leiten wir die CRA-Klassifikation ab.
|
||||
</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-5">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Produktname *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500"
|
||||
placeholder="z.B. SmartHome Gateway v3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Kurzbeschreibung</label>
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
rows={2}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Intended Use — Zweck und Anwendungsbereich
|
||||
</label>
|
||||
<textarea
|
||||
value={intendedUse}
|
||||
onChange={e => setIntendedUse(e.target.value)}
|
||||
rows={3}
|
||||
placeholder="z.B. Mobile App fuer Industrieanlagen-Monitoring, oder: Password Manager fuer KMU, oder: VPN-Software fuer Mitarbeiter-Geraete"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Wichtig fuer die Klassifikation. Erwaehne konkrete Funktionen (z.B. "Firewall", "Betriebssystem") wenn zutreffend.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Repo-URL (optional)</label>
|
||||
<input
|
||||
type="url"
|
||||
value={repoUrl}
|
||||
onChange={e => setRepoUrl(e.target.value)}
|
||||
placeholder="https://github.com/..."
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Primaere Programmiersprache</label>
|
||||
<select
|
||||
value={primaryLanguage}
|
||||
onChange={e => setPrimaryLanguage(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500"
|
||||
>
|
||||
{LANGUAGES.map(l => (
|
||||
<option key={l.value} value={l.value}>{l.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-200 pt-5">
|
||||
<h3 className="text-sm font-medium text-gray-700 mb-3">Eigenschaften des Produkts</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{[
|
||||
['hasFirmware', 'Enthaelt Firmware (Embedded/IoT)', hasFirmware, setHasFirmware],
|
||||
['connectedInternet', 'Mit dem Internet verbunden', connectedInternet, setConnectedInternet],
|
||||
['hasUpdates', 'Hat Software-/Firmware-Updates', hasUpdates, setHasUpdates],
|
||||
['processesPersonal', 'Verarbeitet personenbezogene Daten', processesPersonal, setProcessesPersonal],
|
||||
['isCriticalInfra', 'Zulieferer fuer kritische Infrastruktur', isCriticalInfra, setIsCriticalInfra],
|
||||
].map(([key, label, value, setter]) => (
|
||||
<label key={key as string} className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={value as boolean}
|
||||
onChange={e => (setter as (b: boolean) => void)(e.target.checked)}
|
||||
className="w-4 h-4 rounded border-gray-300 text-red-600 focus:ring-red-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">{label as string}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-3">
|
||||
<button
|
||||
onClick={() => router.push(`/sdk/cra/${projectId}`)}
|
||||
disabled={saving}
|
||||
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
onClick={save}
|
||||
disabled={saving || !name.trim()}
|
||||
className="flex-1 py-2 bg-red-600 text-white font-medium rounded-lg hover:bg-red-700 disabled:bg-gray-300"
|
||||
>
|
||||
{saving ? 'Speichert...' : 'Weiter zum Scope-Check →'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user