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>
196 lines
7.4 KiB
TypeScript
196 lines
7.4 KiB
TypeScript
'use client'
|
|
|
|
import React, { useEffect, useState, useCallback, use } from 'react'
|
|
|
|
interface CheckItem {
|
|
id: string
|
|
check_code: string
|
|
title: string
|
|
description: string
|
|
check_type: string
|
|
target_url: string | null
|
|
linked_req_ids: string[]
|
|
last_run_at: string | null
|
|
is_active: boolean
|
|
latest_result: { status: string; message: string; ran_at: string } | null
|
|
}
|
|
|
|
interface ChecksResponse {
|
|
project_id: string
|
|
total: number
|
|
items: CheckItem[]
|
|
}
|
|
|
|
const STATUS_STYLE: Record<string, string> = {
|
|
pass: 'bg-green-100 text-green-800',
|
|
fail: 'bg-red-100 text-red-800',
|
|
manual_review_required: 'bg-yellow-100 text-yellow-800',
|
|
}
|
|
|
|
export default function ChecksPage({
|
|
params,
|
|
}: {
|
|
params: Promise<{ projectId: string }>
|
|
}) {
|
|
const { projectId } = use(params)
|
|
const [data, setData] = useState<ChecksResponse | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState('')
|
|
const [running, setRunning] = useState<string | null>(null)
|
|
const [urlInputs, setUrlInputs] = useState<Record<string, string>>({})
|
|
|
|
const tenant = '00000000-0000-0000-0000-000000000001'
|
|
|
|
const load = useCallback(async () => {
|
|
try {
|
|
const res = await fetch(`/api/sdk/v1/cra/projects/${projectId}/checks`, {
|
|
headers: { 'X-Tenant-ID': tenant },
|
|
})
|
|
if (!res.ok) throw new Error(await res.text())
|
|
const json: ChecksResponse = await res.json()
|
|
setData(json)
|
|
const u: Record<string, string> = {}
|
|
for (const c of json.items) u[c.id] = c.target_url || ''
|
|
setUrlInputs(u)
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : 'Fehler beim Laden')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, [projectId])
|
|
|
|
useEffect(() => { load() }, [load])
|
|
|
|
const initChecks = async () => {
|
|
setError('')
|
|
try {
|
|
const res = await fetch(`/api/sdk/v1/cra/projects/${projectId}/checks`, {
|
|
method: 'POST',
|
|
headers: { 'X-Tenant-ID': tenant },
|
|
})
|
|
if (!res.ok) throw new Error(await res.text())
|
|
await load()
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : 'Init fehlgeschlagen')
|
|
}
|
|
}
|
|
|
|
const runCheck = async (checkId: string) => {
|
|
setRunning(checkId)
|
|
setError('')
|
|
try {
|
|
const res = await fetch(`/api/sdk/v1/cra/checks/${checkId}/run`, {
|
|
method: 'POST',
|
|
headers: { 'X-Tenant-ID': tenant, 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ target_url: urlInputs[checkId] || null }),
|
|
})
|
|
if (!res.ok) throw new Error(await res.text())
|
|
await load()
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : 'Run fehlgeschlagen')
|
|
} finally {
|
|
setRunning(null)
|
|
}
|
|
}
|
|
|
|
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">
|
|
← Zurueck zum Projekt
|
|
</a>
|
|
<h1 className="text-2xl font-bold text-gray-900 mt-2">Automatisierte Checks</h1>
|
|
<p className="text-sm text-gray-600 mt-1">
|
|
CRA-typische Online-Pruefungen: security.txt, Update-Policy, TLS-Konfiguration, Vuln-Disclosure.
|
|
</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-xs underline mt-1">Schliessen</button>
|
|
</div>
|
|
)}
|
|
|
|
{data && data.items.length === 0 && (
|
|
<div className="bg-white rounded-xl border border-gray-200 p-8 text-center">
|
|
<p className="text-gray-600 mb-3">Noch keine Checks fuer dieses Projekt konfiguriert.</p>
|
|
<button
|
|
onClick={initChecks}
|
|
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 text-sm font-medium"
|
|
>
|
|
Standard-CRA-Checks erstellen (6 Stueck)
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{data && data.items.length > 0 && (
|
|
<div className="space-y-3">
|
|
{data.items.map(c => (
|
|
<div key={c.id} className="bg-white rounded-xl shadow-sm border border-gray-200 p-5">
|
|
<div className="flex items-start justify-between gap-4 mb-3">
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2">
|
|
<h3 className="font-semibold text-gray-900">{c.title}</h3>
|
|
<span className="text-xs text-gray-400">{c.check_code}</span>
|
|
</div>
|
|
<p className="text-sm text-gray-600 mt-1">{c.description}</p>
|
|
{c.linked_req_ids.length > 0 && (
|
|
<div className="mt-2 flex flex-wrap gap-1">
|
|
{c.linked_req_ids.map(r => (
|
|
<span key={r} className="px-2 py-0.5 text-xs rounded bg-blue-100 text-blue-700">{r}</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
{c.latest_result && (
|
|
<span className={`px-2 py-1 text-xs rounded-full font-medium ${STATUS_STYLE[c.latest_result.status] || 'bg-gray-100 text-gray-600'}`}>
|
|
{c.latest_result.status}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{(c.check_type === 'url_probe' || c.check_type === 'tls_probe' || c.check_type === 'manual_review') && (
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<input
|
|
type="url"
|
|
placeholder={c.check_type === 'tls_probe' ? 'https://product.example.com' : 'https://your-product.com'}
|
|
value={urlInputs[c.id] ?? ''}
|
|
onChange={e => setUrlInputs({ ...urlInputs, [c.id]: e.target.value })}
|
|
className="flex-1 px-3 py-1.5 border border-gray-300 rounded text-sm focus:ring-2 focus:ring-red-500"
|
|
/>
|
|
<button
|
|
onClick={() => runCheck(c.id)}
|
|
disabled={running === c.id}
|
|
className="px-3 py-1.5 bg-red-600 text-white text-sm rounded hover:bg-red-700 disabled:bg-gray-300"
|
|
>
|
|
{running === c.id ? 'Laeuft...' : 'Run'}
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{c.latest_result && (
|
|
<div className="mt-2 text-xs text-gray-600 bg-gray-50 rounded p-2 font-mono">
|
|
{c.latest_result.message}
|
|
<div className="text-gray-400 mt-1 text-[10px]">
|
|
Geprueft: {new Date(c.latest_result.ran_at).toLocaleString('de-DE')}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</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> Aktuell implementiert: <code>cra_security_txt</code> (HTTP) und <code>cra_tls_cert_check</code> (TLS-Handshake).
|
|
Andere Check-Typen sind als <code>manual_review_required</code> markiert — der Pruefer beantwortet sie manuell.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|