diff --git a/admin-compliance/app/api/sdk/v1/canonical/route.ts b/admin-compliance/app/api/sdk/v1/canonical/route.ts index e658f5c..bf3540e 100644 --- a/admin-compliance/app/api/sdk/v1/canonical/route.ts +++ b/admin-compliance/app/api/sdk/v1/canonical/route.ts @@ -80,25 +80,73 @@ export async function GET(request: NextRequest) { } /** - * Proxy: POST /api/sdk/v1/canonical?endpoint=similarity-check&id=... + * Proxy: POST /api/sdk/v1/canonical?endpoint=... * - * Routes to: POST /api/compliance/v1/canonical/controls/{id}/similarity-check + * endpoint=create-control → POST /api/compliance/v1/canonical/controls + * endpoint=similarity-check&id= → POST /api/compliance/v1/canonical/controls/{id}/similarity-check */ export async function POST(request: NextRequest) { try { const { searchParams } = new URL(request.url) const endpoint = searchParams.get('endpoint') - const controlId = searchParams.get('id') + const body = await request.json() - if (endpoint !== 'similarity-check' || !controlId) { - return NextResponse.json({ error: 'Invalid endpoint or missing id' }, { status: 400 }) + let backendPath: string + + if (endpoint === 'create-control') { + backendPath = '/api/compliance/v1/canonical/controls' + } else if (endpoint === 'similarity-check') { + const controlId = searchParams.get('id') + if (!controlId) { + return NextResponse.json({ error: 'Missing control id' }, { status: 400 }) + } + backendPath = `/api/compliance/v1/canonical/controls/${encodeURIComponent(controlId)}/similarity-check` + } else { + return NextResponse.json({ error: `Unknown POST endpoint: ${endpoint}` }, { status: 400 }) + } + + const response = await fetch(`${BACKEND_URL}${backendPath}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }) + + if (!response.ok) { + const errorText = await response.text() + return NextResponse.json( + { error: 'Backend error', details: errorText }, + { status: response.status } + ) + } + + return NextResponse.json(await response.json(), { status: response.status }) + } catch (error) { + console.error('Canonical control POST proxy error:', error) + return NextResponse.json( + { error: 'Failed to connect to backend' }, + { status: 503 } + ) + } +} + +/** + * Proxy: PUT /api/sdk/v1/canonical?endpoint=update-control&id=AUTH-001 + * + * Routes to: PUT /api/compliance/v1/canonical/controls/{id} + */ +export async function PUT(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const controlId = searchParams.get('id') + if (!controlId) { + return NextResponse.json({ error: 'Missing control id' }, { status: 400 }) } const body = await request.json() const response = await fetch( - `${BACKEND_URL}/api/compliance/v1/canonical/controls/${encodeURIComponent(controlId)}/similarity-check`, + `${BACKEND_URL}/api/compliance/v1/canonical/controls/${encodeURIComponent(controlId)}`, { - method: 'POST', + method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), } @@ -114,7 +162,43 @@ export async function POST(request: NextRequest) { return NextResponse.json(await response.json()) } catch (error) { - console.error('Similarity check proxy error:', error) + console.error('Canonical control PUT proxy error:', error) + return NextResponse.json( + { error: 'Failed to connect to backend' }, + { status: 503 } + ) + } +} + +/** + * Proxy: DELETE /api/sdk/v1/canonical?id=AUTH-001 + * + * Routes to: DELETE /api/compliance/v1/canonical/controls/{id} + */ +export async function DELETE(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const controlId = searchParams.get('id') + if (!controlId) { + return NextResponse.json({ error: 'Missing control id' }, { status: 400 }) + } + + const response = await fetch( + `${BACKEND_URL}/api/compliance/v1/canonical/controls/${encodeURIComponent(controlId)}`, + { method: 'DELETE' } + ) + + if (!response.ok && response.status !== 204) { + const errorText = await response.text() + return NextResponse.json( + { error: 'Backend error', details: errorText }, + { status: response.status } + ) + } + + return new NextResponse(null, { status: 204 }) + } catch (error) { + console.error('Canonical control DELETE proxy error:', error) return NextResponse.json( { error: 'Failed to connect to backend' }, { status: 503 } diff --git a/admin-compliance/app/sdk/control-library/page.tsx b/admin-compliance/app/sdk/control-library/page.tsx index 656744f..e65df88 100644 --- a/admin-compliance/app/sdk/control-library/page.tsx +++ b/admin-compliance/app/sdk/control-library/page.tsx @@ -4,7 +4,7 @@ import { useState, useEffect, useMemo, useCallback } from 'react' import { Shield, Search, ChevronRight, ArrowLeft, ExternalLink, Filter, AlertTriangle, CheckCircle2, Info, Lock, - FileText, BookOpen, Scale, + FileText, BookOpen, Scale, Plus, Pencil, Trash2, Save, X, } from 'lucide-react' // ============================================================================= @@ -77,6 +77,24 @@ const EFFORT_LABELS: Record = { const BACKEND_URL = '/api/sdk/v1/canonical' +const EMPTY_CONTROL = { + framework_id: 'bp_security_v1', + control_id: '', + title: '', + objective: '', + rationale: '', + scope: { platforms: [] as string[], components: [] as string[], data_classes: [] as string[] }, + requirements: [''], + test_procedure: [''], + evidence: [{ type: '', description: '' }], + severity: 'medium', + risk_score: null as number | null, + implementation_effort: 'm' as string | null, + open_anchors: [{ framework: '', ref: '', url: '' }], + release_state: 'draft', + tags: [] as string[], +} + // ============================================================================= // HELPERS // ============================================================================= @@ -110,6 +128,277 @@ function getDomain(controlId: string): string { return controlId.split('-')[0] || '' } +// ============================================================================= +// CONTROL FORM COMPONENT +// ============================================================================= + +function ControlForm({ + initial, + onSave, + onCancel, + saving, +}: { + initial: typeof EMPTY_CONTROL + onSave: (data: typeof EMPTY_CONTROL) => void + onCancel: () => void + saving: boolean +}) { + const [form, setForm] = useState(initial) + const [tagInput, setTagInput] = useState(initial.tags.join(', ')) + const [platformInput, setPlatformInput] = useState((initial.scope.platforms || []).join(', ')) + const [componentInput, setComponentInput] = useState((initial.scope.components || []).join(', ')) + const [dataClassInput, setDataClassInput] = useState((initial.scope.data_classes || []).join(', ')) + + const handleSave = () => { + const data = { + ...form, + tags: tagInput.split(',').map(t => t.trim()).filter(Boolean), + scope: { + platforms: platformInput.split(',').map(t => t.trim()).filter(Boolean), + components: componentInput.split(',').map(t => t.trim()).filter(Boolean), + data_classes: dataClassInput.split(',').map(t => t.trim()).filter(Boolean), + }, + requirements: form.requirements.filter(r => r.trim()), + test_procedure: form.test_procedure.filter(r => r.trim()), + evidence: form.evidence.filter(e => e.type.trim() || e.description.trim()), + open_anchors: form.open_anchors.filter(a => a.framework.trim() || a.ref.trim()), + } + onSave(data) + } + + return ( +
+
+

+ {initial.control_id ? `Control ${initial.control_id} bearbeiten` : 'Neues Control erstellen'} +

+
+ + +
+
+ + {/* Basic fields */} +
+
+ + setForm({ ...form, control_id: e.target.value.toUpperCase() })} + placeholder="AUTH-003" + disabled={!!initial.control_id} + className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:outline-none disabled:bg-gray-100" + /> +

Format: DOMAIN-NNN (z.B. AUTH-003, NET-005)

+
+
+ + setForm({ ...form, title: e.target.value })} + placeholder="Control-Titel" + className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:outline-none" + /> +
+
+ +
+
+ + +
+
+ + setForm({ ...form, risk_score: e.target.value ? parseFloat(e.target.value) : null })} + className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg" + /> +
+
+ + +
+
+ + {/* Objective & Rationale */} +
+ +