From 1cf5de1d45ddc9ebc3290698c84aba07dc679f41 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Mon, 18 May 2026 17:56:52 +0200 Subject: [PATCH] feat(cra): CRA Compliance module Phase 1+2+3 (intake, scope, path, requirements, backlog, sbom, checks) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../sdk/v1/cra/checks/[checkId]/run/route.ts | 23 + .../sdk/v1/cra/projects/[id]/backlog/route.ts | 20 + .../sdk/v1/cra/projects/[id]/checks/route.ts | 41 + .../v1/cra/projects/[id]/path-select/route.ts | 29 + .../cra/projects/[id]/requirements/route.ts | 20 + .../app/api/sdk/v1/cra/projects/[id]/route.ts | 45 + .../sdk/v1/cra/projects/[id]/sbom/route.ts | 48 + .../v1/cra/projects/[id]/scope-check/route.ts | 24 + .../app/api/sdk/v1/cra/projects/route.ts | 56 + .../app/sdk/cra/[projectId]/backlog/page.tsx | 155 +++ .../app/sdk/cra/[projectId]/checks/page.tsx | 195 +++ .../app/sdk/cra/[projectId]/intake/page.tsx | 240 ++++ .../app/sdk/cra/[projectId]/page.tsx | 332 +++++ .../app/sdk/cra/[projectId]/path/page.tsx | 256 ++++ .../sdk/cra/[projectId]/requirements/page.tsx | 182 +++ .../app/sdk/cra/[projectId]/sbom/page.tsx | 171 +++ .../app/sdk/cra/[projectId]/scope/page.tsx | 172 +++ .../cra/_components/ClassificationBadge.tsx | 24 + .../app/sdk/cra/_components/SeverityBadge.tsx | 13 + .../app/sdk/cra/_components/StatusStepper.tsx | 40 + admin-compliance/app/sdk/cra/page.tsx | 200 +++ .../sdk/Sidebar/SidebarModuleList.tsx | 22 + .../compliance/api/cra_annex_i_data.py | 260 ++++ .../compliance/api/cra_routes.py | 1075 +++++++++++++++++ backend-compliance/main.py | 4 + .../119_compliance_cra_projects.sql | 44 + .../migrations/120_compliance_cra_sboms.sql | 23 + 27 files changed, 3714 insertions(+) create mode 100644 admin-compliance/app/api/sdk/v1/cra/checks/[checkId]/run/route.ts create mode 100644 admin-compliance/app/api/sdk/v1/cra/projects/[id]/backlog/route.ts create mode 100644 admin-compliance/app/api/sdk/v1/cra/projects/[id]/checks/route.ts create mode 100644 admin-compliance/app/api/sdk/v1/cra/projects/[id]/path-select/route.ts create mode 100644 admin-compliance/app/api/sdk/v1/cra/projects/[id]/requirements/route.ts create mode 100644 admin-compliance/app/api/sdk/v1/cra/projects/[id]/route.ts create mode 100644 admin-compliance/app/api/sdk/v1/cra/projects/[id]/sbom/route.ts create mode 100644 admin-compliance/app/api/sdk/v1/cra/projects/[id]/scope-check/route.ts create mode 100644 admin-compliance/app/api/sdk/v1/cra/projects/route.ts create mode 100644 admin-compliance/app/sdk/cra/[projectId]/backlog/page.tsx create mode 100644 admin-compliance/app/sdk/cra/[projectId]/checks/page.tsx create mode 100644 admin-compliance/app/sdk/cra/[projectId]/intake/page.tsx create mode 100644 admin-compliance/app/sdk/cra/[projectId]/page.tsx create mode 100644 admin-compliance/app/sdk/cra/[projectId]/path/page.tsx create mode 100644 admin-compliance/app/sdk/cra/[projectId]/requirements/page.tsx create mode 100644 admin-compliance/app/sdk/cra/[projectId]/sbom/page.tsx create mode 100644 admin-compliance/app/sdk/cra/[projectId]/scope/page.tsx create mode 100644 admin-compliance/app/sdk/cra/_components/ClassificationBadge.tsx create mode 100644 admin-compliance/app/sdk/cra/_components/SeverityBadge.tsx create mode 100644 admin-compliance/app/sdk/cra/_components/StatusStepper.tsx create mode 100644 admin-compliance/app/sdk/cra/page.tsx create mode 100644 backend-compliance/compliance/api/cra_annex_i_data.py create mode 100644 backend-compliance/compliance/api/cra_routes.py create mode 100644 backend-compliance/migrations/119_compliance_cra_projects.sql create mode 100644 backend-compliance/migrations/120_compliance_cra_sboms.sql diff --git a/admin-compliance/app/api/sdk/v1/cra/checks/[checkId]/run/route.ts b/admin-compliance/app/api/sdk/v1/cra/checks/[checkId]/run/route.ts new file mode 100644 index 00000000..69804e9e --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/cra/checks/[checkId]/run/route.ts @@ -0,0 +1,23 @@ +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002' + +export async function POST(request: NextRequest, ctx: { params: Promise<{ checkId: string }> }) { + const { checkId } = await ctx.params + const tenantId = request.headers.get('x-tenant-id') || '00000000-0000-0000-0000-000000000001' + const body = await request.text() + try { + const resp = await fetch(`${BACKEND_URL}/api/v1/cra/projects/checks/${checkId}/run`, { + method: 'POST', + headers: { 'X-Tenant-ID': tenantId, 'Content-Type': 'application/json' }, + body: body || '{}', + }) + const text = await resp.text() + return new NextResponse(text, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }) + } catch (err) { + return NextResponse.json({ error: 'Backend unreachable', details: String(err) }, { status: 502 }) + } +} diff --git a/admin-compliance/app/api/sdk/v1/cra/projects/[id]/backlog/route.ts b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/backlog/route.ts new file mode 100644 index 00000000..e41486df --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/backlog/route.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002' + +export async function GET(request: NextRequest, ctx: { params: Promise<{ id: string }> }) { + const { id } = await ctx.params + const tenantId = request.headers.get('x-tenant-id') || '00000000-0000-0000-0000-000000000001' + try { + const resp = await fetch(`${BACKEND_URL}/api/v1/cra/projects/${id}/backlog`, { + headers: { 'X-Tenant-ID': tenantId }, + }) + const text = await resp.text() + return new NextResponse(text, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }) + } catch (err) { + return NextResponse.json({ error: 'Backend unreachable', details: String(err) }, { status: 502 }) + } +} diff --git a/admin-compliance/app/api/sdk/v1/cra/projects/[id]/checks/route.ts b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/checks/route.ts new file mode 100644 index 00000000..e11ed699 --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/checks/route.ts @@ -0,0 +1,41 @@ +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002' + +function tenant(req: NextRequest) { + return req.headers.get('x-tenant-id') || '00000000-0000-0000-0000-000000000001' +} + +export async function GET(request: NextRequest, ctx: { params: Promise<{ id: string }> }) { + const { id } = await ctx.params + try { + const resp = await fetch(`${BACKEND_URL}/api/v1/cra/projects/${id}/checks`, { + headers: { 'X-Tenant-ID': tenant(request) }, + }) + const text = await resp.text() + return new NextResponse(text, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }) + } catch (err) { + return NextResponse.json({ error: 'Backend unreachable', details: String(err) }, { status: 502 }) + } +} + +/** POST /checks (no body) -> backend /checks/init creates default checks */ +export async function POST(request: NextRequest, ctx: { params: Promise<{ id: string }> }) { + const { id } = await ctx.params + try { + const resp = await fetch(`${BACKEND_URL}/api/v1/cra/projects/${id}/checks/init`, { + method: 'POST', + headers: { 'X-Tenant-ID': tenant(request) }, + }) + const text = await resp.text() + return new NextResponse(text, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }) + } catch (err) { + return NextResponse.json({ error: 'Backend unreachable', details: String(err) }, { status: 502 }) + } +} diff --git a/admin-compliance/app/api/sdk/v1/cra/projects/[id]/path-select/route.ts b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/path-select/route.ts new file mode 100644 index 00000000..71374c49 --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/path-select/route.ts @@ -0,0 +1,29 @@ +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002' + +export async function POST(request: NextRequest, ctx: { params: Promise<{ id: string }> }) { + const { id } = await ctx.params + const tenantId = request.headers.get('x-tenant-id') || '00000000-0000-0000-0000-000000000001' + const body = await request.text() + try { + const resp = await fetch(`${BACKEND_URL}/api/v1/cra/projects/${id}/path-select`, { + method: 'POST', + headers: { + 'X-Tenant-ID': tenantId, + 'Content-Type': 'application/json', + }, + body, + }) + const text = await resp.text() + return new NextResponse(text, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }) + } catch (err) { + return NextResponse.json( + { error: 'Backend unreachable', details: String(err) }, + { status: 502 } + ) + } +} diff --git a/admin-compliance/app/api/sdk/v1/cra/projects/[id]/requirements/route.ts b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/requirements/route.ts new file mode 100644 index 00000000..e03fba70 --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/requirements/route.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002' + +export async function GET(request: NextRequest, ctx: { params: Promise<{ id: string }> }) { + const { id } = await ctx.params + const tenantId = request.headers.get('x-tenant-id') || '00000000-0000-0000-0000-000000000001' + try { + const resp = await fetch(`${BACKEND_URL}/api/v1/cra/projects/${id}/requirements`, { + headers: { 'X-Tenant-ID': tenantId }, + }) + const text = await resp.text() + return new NextResponse(text, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }) + } catch (err) { + return NextResponse.json({ error: 'Backend unreachable', details: String(err) }, { status: 502 }) + } +} diff --git a/admin-compliance/app/api/sdk/v1/cra/projects/[id]/route.ts b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/route.ts new file mode 100644 index 00000000..1bfb78eb --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/route.ts @@ -0,0 +1,45 @@ +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002' + +function tenantHeader(request: NextRequest): string { + return request.headers.get('x-tenant-id') || '00000000-0000-0000-0000-000000000001' +} + +async function proxy(request: NextRequest, method: string, id: string, body?: string) { + const tenantId = tenantHeader(request) + const init: RequestInit = { + method, + headers: { 'X-Tenant-ID': tenantId, 'Content-Type': 'application/json' }, + } + if (body !== undefined) init.body = body + try { + const resp = await fetch(`${BACKEND_URL}/api/v1/cra/projects/${id}`, init) + const text = await resp.text() + return new NextResponse(text, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }) + } catch (err) { + return NextResponse.json( + { error: 'Backend unreachable', details: String(err) }, + { status: 502 } + ) + } +} + +export async function GET(request: NextRequest, ctx: { params: Promise<{ id: string }> }) { + const { id } = await ctx.params + return proxy(request, 'GET', id) +} + +export async function PATCH(request: NextRequest, ctx: { params: Promise<{ id: string }> }) { + const { id } = await ctx.params + const body = await request.text() + return proxy(request, 'PATCH', id, body) +} + +export async function DELETE(request: NextRequest, ctx: { params: Promise<{ id: string }> }) { + const { id } = await ctx.params + return proxy(request, 'DELETE', id) +} diff --git a/admin-compliance/app/api/sdk/v1/cra/projects/[id]/sbom/route.ts b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/sbom/route.ts new file mode 100644 index 00000000..767c331d --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/sbom/route.ts @@ -0,0 +1,48 @@ +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002' + +function tenant(req: NextRequest) { + return req.headers.get('x-tenant-id') || '00000000-0000-0000-0000-000000000001' +} + +/** GET /sbom -> List uploads. We map this to the backend /sboms endpoint. */ +export async function GET(request: NextRequest, ctx: { params: Promise<{ id: string }> }) { + const { id } = await ctx.params + try { + const resp = await fetch(`${BACKEND_URL}/api/v1/cra/projects/${id}/sboms`, { + headers: { 'X-Tenant-ID': tenant(request) }, + }) + const text = await resp.text() + return new NextResponse(text, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }) + } catch (err) { + return NextResponse.json({ error: 'Backend unreachable', details: String(err) }, { status: 502 }) + } +} + +/** POST /sbom -> multipart upload to backend /sbom/upload */ +export async function POST(request: NextRequest, ctx: { params: Promise<{ id: string }> }) { + const { id } = await ctx.params + try { + const formData = await request.formData() + const upstreamForm = new FormData() + for (const [key, value] of formData.entries()) { + upstreamForm.append(key, value) + } + const resp = await fetch(`${BACKEND_URL}/api/v1/cra/projects/${id}/sbom/upload`, { + method: 'POST', + headers: { 'X-Tenant-ID': tenant(request) }, + body: upstreamForm as unknown as BodyInit, + }) + const text = await resp.text() + return new NextResponse(text, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }) + } catch (err) { + return NextResponse.json({ error: 'Backend unreachable', details: String(err) }, { status: 502 }) + } +} diff --git a/admin-compliance/app/api/sdk/v1/cra/projects/[id]/scope-check/route.ts b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/scope-check/route.ts new file mode 100644 index 00000000..44c5dc03 --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/scope-check/route.ts @@ -0,0 +1,24 @@ +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002' + +export async function POST(request: NextRequest, ctx: { params: Promise<{ id: string }> }) { + const { id } = await ctx.params + const tenantId = request.headers.get('x-tenant-id') || '00000000-0000-0000-0000-000000000001' + try { + const resp = await fetch(`${BACKEND_URL}/api/v1/cra/projects/${id}/scope-check`, { + method: 'POST', + headers: { 'X-Tenant-ID': tenantId }, + }) + const text = await resp.text() + return new NextResponse(text, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }) + } catch (err) { + return NextResponse.json( + { error: 'Backend unreachable', details: String(err) }, + { status: 502 } + ) + } +} diff --git a/admin-compliance/app/api/sdk/v1/cra/projects/route.ts b/admin-compliance/app/api/sdk/v1/cra/projects/route.ts new file mode 100644 index 00000000..fcb19ab2 --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/cra/projects/route.ts @@ -0,0 +1,56 @@ +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002' + +function tenantHeader(request: NextRequest): string { + return request.headers.get('x-tenant-id') || '00000000-0000-0000-0000-000000000001' +} + +/** GET /api/sdk/v1/cra/projects -> Backend list */ +export async function GET(request: NextRequest) { + const tenantId = tenantHeader(request) + const { searchParams } = new URL(request.url) + const qs = searchParams.toString() + try { + const resp = await fetch( + `${BACKEND_URL}/api/v1/cra/projects${qs ? `?${qs}` : ''}`, + { headers: { 'X-Tenant-ID': tenantId } } + ) + const body = await resp.text() + return new NextResponse(body, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }) + } catch (err) { + return NextResponse.json( + { error: 'Backend unreachable', details: String(err) }, + { status: 502 } + ) + } +} + +/** POST /api/sdk/v1/cra/projects -> Backend create */ +export async function POST(request: NextRequest) { + const tenantId = tenantHeader(request) + const body = await request.text() + try { + const resp = await fetch(`${BACKEND_URL}/api/v1/cra/projects`, { + method: 'POST', + headers: { + 'X-Tenant-ID': tenantId, + 'Content-Type': 'application/json', + }, + body, + }) + const text = await resp.text() + return new NextResponse(text, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }) + } catch (err) { + return NextResponse.json( + { error: 'Backend unreachable', details: String(err) }, + { status: 502 } + ) + } +} diff --git a/admin-compliance/app/sdk/cra/[projectId]/backlog/page.tsx b/admin-compliance/app/sdk/cra/[projectId]/backlog/page.tsx new file mode 100644 index 00000000..118b162c --- /dev/null +++ b/admin-compliance/app/sdk/cra/[projectId]/backlog/page.tsx @@ -0,0 +1,155 @@ +'use client' + +import React, { useEffect, useState, useCallback, use } from 'react' +import { SeverityBadge } from '../../_components/SeverityBadge' + +interface BacklogItem { + rank: number + req_id: string + title: string + category: string + severity: string + annex_anchor: string + description: string + effort_days: number + mapped_measure_names: { id: string; name: string }[] + status: string + priority_score: number +} + +interface BacklogResponse { + project_id: string + classification: string | null + days_to_ce_deadline: number + deadlines: { date: string; label: string }[] + total: number + items: BacklogItem[] +} + +export default function BacklogPage({ + params, +}: { + params: Promise<{ projectId: string }> +}) { + const { projectId } = use(params) + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState('') + + const load = useCallback(async () => { + try { + const res = await fetch(`/api/sdk/v1/cra/projects/${projectId}/backlog`, { + headers: { 'X-Tenant-ID': '00000000-0000-0000-0000-000000000001' }, + }) + 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]) + + if (loading) return

Laedt...

+ if (error) return

{error}

+ if (!data) return null + + return ( +
+
+
+ + ← Zurueck zum Projekt + +

Prioritaeten-Backlog

+

+ Sortiert nach Severity × Deadline-Druck × Effort. Was du heute tust, was naechsten Sprint, was vor 11.12.2027. +

+
+ + {/* Deadline-Banner */} +
+ {data.deadlines.map(d => { + const days = Math.max(0, Math.round((new Date(d.date).getTime() - Date.now()) / 86400000)) + const isPast = new Date(d.date).getTime() < Date.now() + return ( +
+
{d.date}
+
{d.label}
+
+ {isPast ? 'bereits abgelaufen' : `noch ${days} Tage`} +
+
+ ) + })} +
+ + {/* Backlog */} +
+ + + + + + + + + + + + + + {data.items.map(item => ( + + + + + + + + + + ))} + +
RangAnforderungSeverityScoreAufwandMassnahmeAktion
{item.rank} +
{item.title}
+
{item.category} · {item.annex_anchor}
+
{item.priority_score}{item.effort_days} PT + {item.mapped_measure_names.length > 0 ? ( +
+ {item.mapped_measure_names.map(m => ( +
+ {m.id}: {m.name.length > 50 ? m.name.slice(0, 50) + '...' : m.name} +
+ ))} +
+ ) : ( + + )} +
+ +
+
+ +

+ Tage bis CE-Marking-Pflicht (11.12.2027): {data.days_to_ce_deadline} +

+
+
+ ) +} diff --git a/admin-compliance/app/sdk/cra/[projectId]/checks/page.tsx b/admin-compliance/app/sdk/cra/[projectId]/checks/page.tsx new file mode 100644 index 00000000..2737ad46 --- /dev/null +++ b/admin-compliance/app/sdk/cra/[projectId]/checks/page.tsx @@ -0,0 +1,195 @@ +'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 = { + 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(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState('') + const [running, setRunning] = useState(null) + const [urlInputs, setUrlInputs] = useState>({}) + + 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 = {} + 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

Laedt...

+ + return ( +
+
+
+ + ← Zurueck zum Projekt + +

Automatisierte Checks

+

+ CRA-typische Online-Pruefungen: security.txt, Update-Policy, TLS-Konfiguration, Vuln-Disclosure. +

+
+ + {error && ( +
+
{error}
+ +
+ )} + + {data && data.items.length === 0 && ( +
+

Noch keine Checks fuer dieses Projekt konfiguriert.

+ +
+ )} + + {data && data.items.length > 0 && ( +
+ {data.items.map(c => ( +
+
+
+
+

{c.title}

+ {c.check_code} +
+

{c.description}

+ {c.linked_req_ids.length > 0 && ( +
+ {c.linked_req_ids.map(r => ( + {r} + ))} +
+ )} +
+ {c.latest_result && ( + + {c.latest_result.status} + + )} +
+ + {(c.check_type === 'url_probe' || c.check_type === 'tls_probe' || c.check_type === 'manual_review') && ( +
+ 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" + /> + +
+ )} + + {c.latest_result && ( +
+ {c.latest_result.message} +
+ Geprueft: {new Date(c.latest_result.ran_at).toLocaleString('de-DE')} +
+
+ )} +
+ ))} +
+ )} + +
+ Hinweis: Aktuell implementiert: cra_security_txt (HTTP) und cra_tls_cert_check (TLS-Handshake). + Andere Check-Typen sind als manual_review_required markiert — der Pruefer beantwortet sie manuell. +
+
+
+ ) +} diff --git a/admin-compliance/app/sdk/cra/[projectId]/intake/page.tsx b/admin-compliance/app/sdk/cra/[projectId]/intake/page.tsx new file mode 100644 index 00000000..6dfd0a07 --- /dev/null +++ b/admin-compliance/app/sdk/cra/[projectId]/intake/page.tsx @@ -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

Laedt...

+ + return ( +
+
+
+ + ← Zurueck zum Projekt + +

Intake — Software-Profil

+

+ Schritt 1 von 3 — Beschreibe Software, Firmware und Connectivity. Daraus leiten wir die CRA-Klassifikation ab. +

+
+ + {error && ( +
{error}
+ )} + +
+
+ + 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" + /> +
+ +
+ +