Files
breakpilot-compliance/admin-compliance/app/sdk/cra/[projectId]/sbom/page.tsx
T
Benjamin Admin 1cf5de1d45 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>
2026-05-18 17:56:52 +02:00

172 lines
6.6 KiB
TypeScript

'use client'
import React, { useEffect, useState, useCallback, use, useRef } from 'react'
interface SBOMItem {
id: string
filename: string
format: string
spec_version: string | null
component_count: number
summary: Record<string, unknown>
scan_status: string
scan_summary: Record<string, unknown>
uploaded_at: string
scanned_at: string | null
}
interface SBOMListResponse {
project_id: string
total: number
items: SBOMItem[]
}
export default function SBOMPage({
params,
}: {
params: Promise<{ projectId: string }>
}) {
const { projectId } = use(params)
const [data, setData] = useState<SBOMListResponse | null>(null)
const [loading, setLoading] = useState(true)
const [uploading, setUploading] = useState(false)
const [error, setError] = useState('')
const fileRef = useRef<HTMLInputElement>(null)
const tenant = '00000000-0000-0000-0000-000000000001'
const load = useCallback(async () => {
try {
const res = await fetch(`/api/sdk/v1/cra/projects/${projectId}/sbom`, {
headers: { 'X-Tenant-ID': tenant },
})
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])
const onUpload = async () => {
const f = fileRef.current?.files?.[0]
if (!f) return
setUploading(true)
setError('')
try {
const fd = new FormData()
fd.append('file', f)
const res = await fetch(`/api/sdk/v1/cra/projects/${projectId}/sbom`, {
method: 'POST',
headers: { 'X-Tenant-ID': tenant },
body: fd,
})
if (!res.ok) throw new Error(await res.text())
if (fileRef.current) fileRef.current.value = ''
await load()
} catch (e) {
setError(e instanceof Error ? e.message : 'Upload fehlgeschlagen')
} finally {
setUploading(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-5xl mx-auto px-4">
<div className="mb-6">
<a href={`/sdk/cra/${projectId}`} className="text-sm text-blue-600 hover:underline">
&larr; Zurueck zum Projekt
</a>
<h1 className="text-2xl font-bold text-gray-900 mt-2">SBOM Software Bill of Materials</h1>
<p className="text-sm text-gray-600 mt-1">
CycloneDX oder SPDX hochladen. Verknuepft mit Annex-I Requirement 23 (SBOM-Pflicht).
</p>
</div>
{error && (
<div className="mb-4 bg-red-50 border border-red-200 rounded-lg p-3 text-sm text-red-700">
<pre className="whitespace-pre-wrap">{error}</pre>
<button onClick={() => setError('')} className="text-red-500 mt-1 underline text-xs">Schliessen</button>
</div>
)}
<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 mb-3">Neue Version hochladen</h3>
<div className="flex items-center gap-3">
<input
ref={fileRef}
type="file"
accept=".json,application/json"
className="flex-1 text-sm file:mr-3 file:py-1.5 file:px-3 file:rounded file:border-0 file:text-sm file:bg-blue-100 file:text-blue-700 hover:file:bg-blue-200"
/>
<button
onClick={onUpload}
disabled={uploading}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 disabled:bg-gray-300 text-sm font-medium"
>
{uploading ? 'Laedt hoch...' : 'Upload'}
</button>
</div>
<p className="text-xs text-gray-500 mt-2">
Format: CycloneDX-JSON (mit <code>bomFormat: &quot;CycloneDX&quot;</code>) oder SPDX-JSON (mit <code>spdxVersion</code>).
Generieren z.B. via <code>npx @cyclonedx/cyclonedx-npm</code> oder <code>cyclonedx-py</code>.
</p>
</div>
{data && data.items.length === 0 && (
<div className="bg-gray-100 rounded-xl p-8 text-center text-gray-500">
Noch kein SBOM hochgeladen.
</div>
)}
{data && data.items.length > 0 && (
<div className="space-y-3">
<h3 className="text-sm font-semibold text-gray-700">Versionen ({data.total})</h3>
{data.items.map(s => (
<div key={s.id} className="bg-white rounded-xl shadow-sm border border-gray-200 p-4">
<div className="flex items-center justify-between gap-4">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-semibold text-gray-900">{s.filename}</span>
<span className="px-2 py-0.5 text-xs rounded bg-blue-100 text-blue-700 uppercase">{s.format}</span>
{s.spec_version && (
<span className="text-xs text-gray-500">v{s.spec_version}</span>
)}
</div>
<div className="text-xs text-gray-500 mt-1">
{s.component_count} Komponenten · hochgeladen {new Date(s.uploaded_at).toLocaleString('de-DE')}
</div>
</div>
<span className={`px-2 py-1 text-xs rounded-full ${
s.scan_status === 'scanned' ? 'bg-green-100 text-green-700' :
s.scan_status === 'failed' ? 'bg-red-100 text-red-700' :
'bg-gray-100 text-gray-600'
}`}>
Scan: {s.scan_status}
</span>
</div>
{s.summary && Object.keys(s.summary).length > 0 && (
<details className="mt-3 text-xs">
<summary className="cursor-pointer text-gray-600 hover:text-gray-900">Summary-Details</summary>
<pre className="mt-2 p-2 bg-gray-50 rounded overflow-x-auto text-xs">{JSON.stringify(s.summary, null, 2)}</pre>
</details>
)}
</div>
))}
</div>
)}
<div className="mt-6 bg-blue-50 border border-blue-200 rounded-xl p-4 text-sm text-blue-900">
<strong>Hinweis:</strong> Der osv.dev-Vulnerability-Scan wird durch ein separates Tool im Team durchgefuehrt.
Diese Seite akzeptiert SBOM-Uploads und persistiert sie versioniert.
</div>
</div>
</div>
)
}