Files
breakpilot-compliance/admin-compliance/app/sdk/control-library/page.tsx
Benjamin Admin c87f07c99a
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 39s
CI/CD / test-python-backend-compliance (push) Successful in 39s
CI/CD / test-python-document-crawler (push) Successful in 30s
CI/CD / test-python-dsms-gateway (push) Successful in 20s
CI/CD / validate-canonical-controls (push) Successful in 12s
CI/CD / deploy-hetzner (push) Successful in 1m37s
feat: seed 10 canonical controls + CRUD endpoints + frontend editor
- Migration 045: Seed 10 controls (AUTH, NET, SUP, LOG, WEB, DATA, CRYP, REL)
  with 39 open-source anchors into the database
- Backend: POST/PUT/DELETE endpoints for canonical controls CRUD
- Frontend proxy: PUT and DELETE methods added to canonical route
- Frontend: Control Library with create/edit/delete UI, full form with
  open anchor management, scope, requirements, evidence, test procedures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 00:28:21 +01:00

885 lines
37 KiB
TypeScript

'use client'
import { useState, useEffect, useMemo, useCallback } from 'react'
import {
Shield, Search, ChevronRight, ArrowLeft, ExternalLink,
Filter, AlertTriangle, CheckCircle2, Info, Lock,
FileText, BookOpen, Scale, Plus, Pencil, Trash2, Save, X,
} from 'lucide-react'
// =============================================================================
// TYPES
// =============================================================================
interface OpenAnchor {
framework: string
ref: string
url: string
}
interface EvidenceItem {
type: string
description: string
}
interface CanonicalControl {
id: string
framework_id: string
control_id: string
title: string
objective: string
rationale: string
scope: {
platforms?: string[]
components?: string[]
data_classes?: string[]
}
requirements: string[]
test_procedure: string[]
evidence: EvidenceItem[]
severity: string
risk_score: number | null
implementation_effort: string | null
evidence_confidence: number | null
open_anchors: OpenAnchor[]
release_state: string
tags: string[]
created_at: string
updated_at: string
}
interface Framework {
id: string
framework_id: string
name: string
version: string
description: string
release_state: string
}
// =============================================================================
// CONSTANTS
// =============================================================================
const SEVERITY_CONFIG: Record<string, { bg: string; label: string; icon: React.ComponentType<{ className?: string }> }> = {
critical: { bg: 'bg-red-100 text-red-800', label: 'Kritisch', icon: AlertTriangle },
high: { bg: 'bg-orange-100 text-orange-800', label: 'Hoch', icon: AlertTriangle },
medium: { bg: 'bg-yellow-100 text-yellow-800', label: 'Mittel', icon: Info },
low: { bg: 'bg-green-100 text-green-800', label: 'Niedrig', icon: CheckCircle2 },
}
const EFFORT_LABELS: Record<string, string> = {
s: 'Klein (S)',
m: 'Mittel (M)',
l: 'Gross (L)',
xl: 'Sehr gross (XL)',
}
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
// =============================================================================
function SeverityBadge({ severity }: { severity: string }) {
const config = SEVERITY_CONFIG[severity] || SEVERITY_CONFIG.medium
const Icon = config.icon
return (
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium ${config.bg}`}>
<Icon className="w-3 h-3" />
{config.label}
</span>
)
}
function StateBadge({ state }: { state: string }) {
const config: Record<string, string> = {
draft: 'bg-gray-100 text-gray-600',
review: 'bg-blue-100 text-blue-700',
approved: 'bg-green-100 text-green-700',
deprecated: 'bg-red-100 text-red-600',
}
return (
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${config[state] || config.draft}`}>
{state}
</span>
)
}
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 (
<div className="max-w-4xl mx-auto p-6 space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-900">
{initial.control_id ? `Control ${initial.control_id} bearbeiten` : 'Neues Control erstellen'}
</h2>
<div className="flex items-center gap-2">
<button onClick={onCancel} className="px-3 py-1.5 text-sm text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50">
<X className="w-4 h-4 inline mr-1" />Abbrechen
</button>
<button onClick={handleSave} disabled={saving} className="px-3 py-1.5 text-sm text-white bg-purple-600 rounded-lg hover:bg-purple-700 disabled:opacity-50">
<Save className="w-4 h-4 inline mr-1" />{saving ? 'Speichern...' : 'Speichern'}
</button>
</div>
</div>
{/* Basic fields */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Control-ID *</label>
<input
value={form.control_id}
onChange={e => 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"
/>
<p className="text-xs text-gray-400 mt-1">Format: DOMAIN-NNN (z.B. AUTH-003, NET-005)</p>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Titel *</label>
<input
value={form.title}
onChange={e => 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"
/>
</div>
</div>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Schweregrad</label>
<select value={form.severity} onChange={e => setForm({ ...form, severity: e.target.value })} className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg">
<option value="low">Niedrig</option>
<option value="medium">Mittel</option>
<option value="high">Hoch</option>
<option value="critical">Kritisch</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Risiko-Score (0-10)</label>
<input
type="number" min="0" max="10" step="0.5"
value={form.risk_score ?? ''}
onChange={e => 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"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Aufwand</label>
<select value={form.implementation_effort || ''} onChange={e => setForm({ ...form, implementation_effort: e.target.value || null })} className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg">
<option value="">-</option>
<option value="s">Klein (S)</option>
<option value="m">Mittel (M)</option>
<option value="l">Gross (L)</option>
<option value="xl">Sehr gross (XL)</option>
</select>
</div>
</div>
{/* Objective & Rationale */}
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Ziel *</label>
<textarea
value={form.objective}
onChange={e => setForm({ ...form, objective: e.target.value })}
rows={3}
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"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Begruendung *</label>
<textarea
value={form.rationale}
onChange={e => setForm({ ...form, rationale: e.target.value })}
rows={3}
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"
/>
</div>
{/* Scope */}
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Plattformen (komma-getrennt)</label>
<input value={platformInput} onChange={e => setPlatformInput(e.target.value)} placeholder="web, mobile, api" className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg" />
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Komponenten (komma-getrennt)</label>
<input value={componentInput} onChange={e => setComponentInput(e.target.value)} placeholder="auth-service, gateway" className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg" />
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Datenklassen (komma-getrennt)</label>
<input value={dataClassInput} onChange={e => setDataClassInput(e.target.value)} placeholder="credentials, tokens" className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg" />
</div>
</div>
{/* Requirements */}
<div>
<div className="flex items-center justify-between mb-1">
<label className="text-xs font-medium text-gray-600">Anforderungen</label>
<button onClick={() => setForm({ ...form, requirements: [...form.requirements, ''] })} className="text-xs text-purple-600 hover:text-purple-800">+ Hinzufuegen</button>
</div>
{form.requirements.map((req, i) => (
<div key={i} className="flex gap-2 mb-2">
<span className="text-xs text-gray-400 mt-2.5">{i + 1}.</span>
<input
value={req}
onChange={e => { const r = [...form.requirements]; r[i] = e.target.value; setForm({ ...form, requirements: r }) }}
className="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-lg"
/>
<button onClick={() => setForm({ ...form, requirements: form.requirements.filter((_, j) => j !== i) })} className="text-red-400 hover:text-red-600">
<Trash2 className="w-4 h-4" />
</button>
</div>
))}
</div>
{/* Test Procedure */}
<div>
<div className="flex items-center justify-between mb-1">
<label className="text-xs font-medium text-gray-600">Pruefverfahren</label>
<button onClick={() => setForm({ ...form, test_procedure: [...form.test_procedure, ''] })} className="text-xs text-purple-600 hover:text-purple-800">+ Hinzufuegen</button>
</div>
{form.test_procedure.map((step, i) => (
<div key={i} className="flex gap-2 mb-2">
<span className="text-xs text-gray-400 mt-2.5">{i + 1}.</span>
<input
value={step}
onChange={e => { const t = [...form.test_procedure]; t[i] = e.target.value; setForm({ ...form, test_procedure: t }) }}
className="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-lg"
/>
<button onClick={() => setForm({ ...form, test_procedure: form.test_procedure.filter((_, j) => j !== i) })} className="text-red-400 hover:text-red-600">
<Trash2 className="w-4 h-4" />
</button>
</div>
))}
</div>
{/* Evidence */}
<div>
<div className="flex items-center justify-between mb-1">
<label className="text-xs font-medium text-gray-600">Nachweisanforderungen</label>
<button onClick={() => setForm({ ...form, evidence: [...form.evidence, { type: '', description: '' }] })} className="text-xs text-purple-600 hover:text-purple-800">+ Hinzufuegen</button>
</div>
{form.evidence.map((ev, i) => (
<div key={i} className="flex gap-2 mb-2">
<input
value={ev.type}
onChange={e => { const evs = [...form.evidence]; evs[i] = { ...evs[i], type: e.target.value }; setForm({ ...form, evidence: evs }) }}
placeholder="Typ (z.B. config, test_result)"
className="w-32 px-3 py-2 text-sm border border-gray-300 rounded-lg"
/>
<input
value={ev.description}
onChange={e => { const evs = [...form.evidence]; evs[i] = { ...evs[i], description: e.target.value }; setForm({ ...form, evidence: evs }) }}
placeholder="Beschreibung"
className="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-lg"
/>
<button onClick={() => setForm({ ...form, evidence: form.evidence.filter((_, j) => j !== i) })} className="text-red-400 hover:text-red-600">
<Trash2 className="w-4 h-4" />
</button>
</div>
))}
</div>
{/* Open Anchors */}
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<BookOpen className="w-4 h-4 text-green-700" />
<label className="text-xs font-semibold text-green-900">Open-Source-Referenzen *</label>
</div>
<button onClick={() => setForm({ ...form, open_anchors: [...form.open_anchors, { framework: '', ref: '', url: '' }] })} className="text-xs text-green-700 hover:text-green-900">+ Hinzufuegen</button>
</div>
<p className="text-xs text-green-600 mb-3">Jedes Control braucht mindestens eine offene Referenz (OWASP, NIST, ENISA, etc.)</p>
{form.open_anchors.map((anchor, i) => (
<div key={i} className="flex gap-2 mb-2">
<input
value={anchor.framework}
onChange={e => { const a = [...form.open_anchors]; a[i] = { ...a[i], framework: e.target.value }; setForm({ ...form, open_anchors: a }) }}
placeholder="Framework (z.B. OWASP ASVS)"
className="w-40 px-3 py-2 text-sm border border-green-200 rounded-lg bg-white"
/>
<input
value={anchor.ref}
onChange={e => { const a = [...form.open_anchors]; a[i] = { ...a[i], ref: e.target.value }; setForm({ ...form, open_anchors: a }) }}
placeholder="Referenz (z.B. V2.8)"
className="w-48 px-3 py-2 text-sm border border-green-200 rounded-lg bg-white"
/>
<input
value={anchor.url}
onChange={e => { const a = [...form.open_anchors]; a[i] = { ...a[i], url: e.target.value }; setForm({ ...form, open_anchors: a }) }}
placeholder="https://..."
className="flex-1 px-3 py-2 text-sm border border-green-200 rounded-lg bg-white"
/>
<button onClick={() => setForm({ ...form, open_anchors: form.open_anchors.filter((_, j) => j !== i) })} className="text-red-400 hover:text-red-600">
<Trash2 className="w-4 h-4" />
</button>
</div>
))}
</div>
{/* Tags & State */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Tags (komma-getrennt)</label>
<input value={tagInput} onChange={e => setTagInput(e.target.value)} placeholder="mfa, auth, iam" className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg" />
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Status</label>
<select value={form.release_state} onChange={e => setForm({ ...form, release_state: e.target.value })} className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg">
<option value="draft">Draft</option>
<option value="review">Review</option>
<option value="approved">Approved</option>
<option value="deprecated">Deprecated</option>
</select>
</div>
</div>
</div>
)
}
// =============================================================================
// CONTROL LIBRARY PAGE
// =============================================================================
export default function ControlLibraryPage() {
const [frameworks, setFrameworks] = useState<Framework[]>([])
const [controls, setControls] = useState<CanonicalControl[]>([])
const [selectedControl, setSelectedControl] = useState<CanonicalControl | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// Filters
const [searchQuery, setSearchQuery] = useState('')
const [severityFilter, setSeverityFilter] = useState<string>('')
const [domainFilter, setDomainFilter] = useState<string>('')
// CRUD state
const [mode, setMode] = useState<'list' | 'detail' | 'create' | 'edit'>('list')
const [saving, setSaving] = useState(false)
// Load data
const loadData = useCallback(async () => {
try {
setLoading(true)
const [fwRes, ctrlRes] = await Promise.all([
fetch(`${BACKEND_URL}?endpoint=frameworks`),
fetch(`${BACKEND_URL}?endpoint=controls`),
])
if (fwRes.ok) setFrameworks(await fwRes.json())
if (ctrlRes.ok) setControls(await ctrlRes.json())
} catch (err) {
setError(err instanceof Error ? err.message : 'Fehler beim Laden')
} finally {
setLoading(false)
}
}, [])
useEffect(() => { loadData() }, [loadData])
// Derived: unique domains
const domains = useMemo(() => {
const set = new Set(controls.map(c => getDomain(c.control_id)))
return Array.from(set).sort()
}, [controls])
// Filtered controls
const filteredControls = useMemo(() => {
return controls.filter(c => {
if (severityFilter && c.severity !== severityFilter) return false
if (domainFilter && getDomain(c.control_id) !== domainFilter) return false
if (searchQuery) {
const q = searchQuery.toLowerCase()
return (
c.control_id.toLowerCase().includes(q) ||
c.title.toLowerCase().includes(q) ||
c.objective.toLowerCase().includes(q) ||
c.tags.some(t => t.toLowerCase().includes(q))
)
}
return true
})
}, [controls, severityFilter, domainFilter, searchQuery])
// CRUD handlers
const handleCreate = async (data: typeof EMPTY_CONTROL) => {
setSaving(true)
try {
const res = await fetch(`${BACKEND_URL}?endpoint=create-control`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (!res.ok) {
const err = await res.json()
alert(`Fehler: ${err.error || err.details || 'Unbekannt'}`)
return
}
await loadData()
setMode('list')
} catch {
alert('Netzwerkfehler')
} finally {
setSaving(false)
}
}
const handleUpdate = async (data: typeof EMPTY_CONTROL) => {
if (!selectedControl) return
setSaving(true)
try {
const res = await fetch(`${BACKEND_URL}?endpoint=update-control&id=${selectedControl.control_id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (!res.ok) {
const err = await res.json()
alert(`Fehler: ${err.error || err.details || 'Unbekannt'}`)
return
}
await loadData()
setSelectedControl(null)
setMode('list')
} catch {
alert('Netzwerkfehler')
} finally {
setSaving(false)
}
}
const handleDelete = async (controlId: string) => {
if (!confirm(`Control ${controlId} wirklich loeschen?`)) return
try {
const res = await fetch(`${BACKEND_URL}?id=${controlId}`, { method: 'DELETE' })
if (!res.ok && res.status !== 204) {
alert('Fehler beim Loeschen')
return
}
await loadData()
setSelectedControl(null)
setMode('list')
} catch {
alert('Netzwerkfehler')
}
}
if (loading) {
return (
<div className="flex items-center justify-center h-96">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-purple-600 border-t-transparent" />
</div>
)
}
if (error) {
return (
<div className="p-6">
<div className="bg-red-50 border border-red-200 rounded-lg p-4 text-red-800 text-sm">{error}</div>
</div>
)
}
// =========================================================================
// CREATE MODE
// =========================================================================
if (mode === 'create') {
return <ControlForm initial={EMPTY_CONTROL} onSave={handleCreate} onCancel={() => setMode('list')} saving={saving} />
}
// =========================================================================
// EDIT MODE
// =========================================================================
if (mode === 'edit' && selectedControl) {
const editData = {
framework_id: frameworks[0]?.framework_id || 'bp_security_v1',
control_id: selectedControl.control_id,
title: selectedControl.title,
objective: selectedControl.objective,
rationale: selectedControl.rationale,
scope: selectedControl.scope || { platforms: [], components: [], data_classes: [] },
requirements: selectedControl.requirements.length ? selectedControl.requirements : [''],
test_procedure: selectedControl.test_procedure.length ? selectedControl.test_procedure : [''],
evidence: selectedControl.evidence.length ? selectedControl.evidence : [{ type: '', description: '' }],
severity: selectedControl.severity,
risk_score: selectedControl.risk_score,
implementation_effort: selectedControl.implementation_effort,
open_anchors: selectedControl.open_anchors.length ? selectedControl.open_anchors : [{ framework: '', ref: '', url: '' }],
release_state: selectedControl.release_state,
tags: selectedControl.tags,
}
return <ControlForm initial={editData} onSave={handleUpdate} onCancel={() => { setMode('detail') }} saving={saving} />
}
// =========================================================================
// DETAIL VIEW
// =========================================================================
if (mode === 'detail' && selectedControl) {
const ctrl = selectedControl
return (
<div className="max-w-4xl mx-auto p-6">
<div className="flex items-center justify-between mb-6">
<button onClick={() => { setSelectedControl(null); setMode('list') }} className="flex items-center gap-1 text-sm text-gray-500 hover:text-gray-700">
<ArrowLeft className="w-4 h-4" /> Zurueck zur Uebersicht
</button>
<div className="flex items-center gap-2">
<button onClick={() => setMode('edit')} className="flex items-center gap-1 px-3 py-1.5 text-sm text-purple-600 border border-purple-300 rounded-lg hover:bg-purple-50">
<Pencil className="w-3.5 h-3.5" /> Bearbeiten
</button>
<button onClick={() => handleDelete(ctrl.control_id)} className="flex items-center gap-1 px-3 py-1.5 text-sm text-red-600 border border-red-300 rounded-lg hover:bg-red-50">
<Trash2 className="w-3.5 h-3.5" /> Loeschen
</button>
</div>
</div>
{/* Header */}
<div className="flex items-start gap-4 mb-6">
<div className="flex-shrink-0 w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center">
<Shield className="w-6 h-6 text-purple-600" />
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="text-sm font-mono text-purple-600">{ctrl.control_id}</span>
<SeverityBadge severity={ctrl.severity} />
<StateBadge state={ctrl.release_state} />
</div>
<h1 className="text-xl font-bold text-gray-900">{ctrl.title}</h1>
<div className="flex items-center gap-4 mt-2 text-xs text-gray-500">
{ctrl.risk_score !== null && <span>Risiko-Score: {ctrl.risk_score}/10</span>}
{ctrl.implementation_effort && <span>Aufwand: {EFFORT_LABELS[ctrl.implementation_effort] || ctrl.implementation_effort}</span>}
</div>
</div>
</div>
{/* Objective & Rationale */}
<div className="space-y-6">
<section>
<h3 className="text-sm font-semibold text-gray-900 mb-2">Ziel</h3>
<p className="text-sm text-gray-700 bg-gray-50 rounded-lg p-4">{ctrl.objective}</p>
</section>
<section>
<h3 className="text-sm font-semibold text-gray-900 mb-2">Begruendung</h3>
<p className="text-sm text-gray-700 bg-gray-50 rounded-lg p-4">{ctrl.rationale}</p>
</section>
{/* Scope */}
<section>
<h3 className="text-sm font-semibold text-gray-900 mb-2">Geltungsbereich</h3>
<div className="grid grid-cols-3 gap-4">
{ctrl.scope.platforms && ctrl.scope.platforms.length > 0 && (
<div>
<p className="text-xs font-medium text-gray-500 mb-1">Plattformen</p>
<div className="flex flex-wrap gap-1">
{ctrl.scope.platforms.map(p => (
<span key={p} className="px-2 py-0.5 bg-blue-50 text-blue-700 rounded text-xs">{p}</span>
))}
</div>
</div>
)}
{ctrl.scope.components && ctrl.scope.components.length > 0 && (
<div>
<p className="text-xs font-medium text-gray-500 mb-1">Komponenten</p>
<div className="flex flex-wrap gap-1">
{ctrl.scope.components.map(c => (
<span key={c} className="px-2 py-0.5 bg-purple-50 text-purple-700 rounded text-xs">{c}</span>
))}
</div>
</div>
)}
{ctrl.scope.data_classes && ctrl.scope.data_classes.length > 0 && (
<div>
<p className="text-xs font-medium text-gray-500 mb-1">Datenklassen</p>
<div className="flex flex-wrap gap-1">
{ctrl.scope.data_classes.map(d => (
<span key={d} className="px-2 py-0.5 bg-amber-50 text-amber-700 rounded text-xs">{d}</span>
))}
</div>
</div>
)}
</div>
</section>
{/* Requirements */}
<section>
<h3 className="text-sm font-semibold text-gray-900 mb-2">Anforderungen</h3>
<ol className="space-y-2">
{ctrl.requirements.map((req, i) => (
<li key={i} className="flex items-start gap-2 text-sm text-gray-700">
<span className="flex-shrink-0 w-5 h-5 bg-purple-100 text-purple-700 rounded-full flex items-center justify-center text-xs font-medium mt-0.5">{i + 1}</span>
{req}
</li>
))}
</ol>
</section>
{/* Test Procedure */}
<section>
<h3 className="text-sm font-semibold text-gray-900 mb-2">Pruefverfahren</h3>
<ol className="space-y-2">
{ctrl.test_procedure.map((step, i) => (
<li key={i} className="flex items-start gap-2 text-sm text-gray-700">
<CheckCircle2 className="w-4 h-4 text-green-500 flex-shrink-0 mt-0.5" />
{step}
</li>
))}
</ol>
</section>
{/* Evidence */}
<section>
<h3 className="text-sm font-semibold text-gray-900 mb-2">Nachweisanforderungen</h3>
<div className="space-y-2">
{ctrl.evidence.map((ev, i) => (
<div key={i} className="flex items-start gap-2 p-3 bg-gray-50 rounded-lg">
<FileText className="w-4 h-4 text-gray-400 flex-shrink-0 mt-0.5" />
<div>
<span className="text-xs font-medium text-gray-500 uppercase">{ev.type}</span>
<p className="text-sm text-gray-700">{ev.description}</p>
</div>
</div>
))}
</div>
</section>
{/* Open Anchors — THE KEY SECTION */}
<section className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="flex items-center gap-2 mb-3">
<BookOpen className="w-4 h-4 text-green-700" />
<h3 className="text-sm font-semibold text-green-900">Open-Source-Referenzen</h3>
<span className="text-xs text-green-600">({ctrl.open_anchors.length} Quellen)</span>
</div>
<p className="text-xs text-green-700 mb-3">
Dieses Control basiert auf frei verfuegbarem Wissen. Alle Referenzen sind offen und oeffentlich zugaenglich.
</p>
<div className="space-y-2">
{ctrl.open_anchors.map((anchor, i) => (
<div key={i} className="flex items-start gap-3 p-2 bg-white rounded border border-green-100">
<Scale className="w-4 h-4 text-green-600 flex-shrink-0 mt-0.5" />
<div className="flex-1 min-w-0">
<span className="text-xs font-semibold text-green-800">{anchor.framework}</span>
<p className="text-sm text-gray-700">{anchor.ref}</p>
</div>
<a
href={anchor.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-xs text-green-600 hover:text-green-800 flex-shrink-0"
>
<ExternalLink className="w-3 h-3" />
Quelle
</a>
</div>
))}
</div>
</section>
{/* Tags */}
{ctrl.tags.length > 0 && (
<section>
<h3 className="text-sm font-semibold text-gray-900 mb-2">Tags</h3>
<div className="flex flex-wrap gap-1.5">
{ctrl.tags.map(tag => (
<span key={tag} className="px-2 py-1 bg-gray-100 text-gray-600 rounded text-xs">{tag}</span>
))}
</div>
</section>
)}
</div>
</div>
)
}
// =========================================================================
// LIST VIEW
// =========================================================================
return (
<div className="flex flex-col h-full">
{/* Header */}
<div className="border-b border-gray-200 bg-white px-6 py-4">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<Shield className="w-6 h-6 text-purple-600" />
<div>
<h1 className="text-lg font-semibold text-gray-900">Canonical Control Library</h1>
<p className="text-xs text-gray-500">
{controls.length} unabhaengig formulierte Security Controls {' '}
{controls.reduce((sum, c) => sum + c.open_anchors.length, 0)} Open-Source-Referenzen
</p>
</div>
</div>
<button
onClick={() => setMode('create')}
className="flex items-center gap-1.5 px-3 py-2 text-sm text-white bg-purple-600 rounded-lg hover:bg-purple-700"
>
<Plus className="w-4 h-4" />
Neues Control
</button>
</div>
{/* Frameworks */}
{frameworks.length > 0 && (
<div className="mb-4 p-3 bg-purple-50 rounded-lg">
<div className="flex items-center gap-2 text-xs text-purple-700">
<Lock className="w-3 h-3" />
<span className="font-medium">{frameworks[0]?.name} v{frameworks[0]?.version}</span>
<span className="text-purple-500"></span>
<span>{frameworks[0]?.description}</span>
</div>
</div>
)}
{/* Filters */}
<div className="flex items-center gap-3">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="text"
placeholder="Controls durchsuchen..."
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
className="w-full pl-9 pr-4 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
/>
</div>
<div className="flex items-center gap-1">
<Filter className="w-4 h-4 text-gray-400" />
</div>
<select
value={severityFilter}
onChange={e => setSeverityFilter(e.target.value)}
className="text-sm border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-purple-500"
>
<option value="">Alle Schweregrade</option>
<option value="critical">Kritisch</option>
<option value="high">Hoch</option>
<option value="medium">Mittel</option>
<option value="low">Niedrig</option>
</select>
<select
value={domainFilter}
onChange={e => setDomainFilter(e.target.value)}
className="text-sm border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-purple-500"
>
<option value="">Alle Domains</option>
{domains.map(d => (
<option key={d} value={d}>{d}</option>
))}
</select>
</div>
</div>
{/* Control List */}
<div className="flex-1 overflow-y-auto p-6">
<div className="space-y-3">
{filteredControls.map(ctrl => (
<button
key={ctrl.control_id}
onClick={() => { setSelectedControl(ctrl); setMode('detail') }}
className="w-full text-left bg-white border border-gray-200 rounded-lg p-4 hover:border-purple-300 hover:shadow-sm transition-all group"
>
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="text-xs font-mono text-purple-600 bg-purple-50 px-1.5 py-0.5 rounded">{ctrl.control_id}</span>
<SeverityBadge severity={ctrl.severity} />
<StateBadge state={ctrl.release_state} />
{ctrl.risk_score !== null && (
<span className="text-xs text-gray-400">Score: {ctrl.risk_score}</span>
)}
</div>
<h3 className="text-sm font-medium text-gray-900 group-hover:text-purple-700">{ctrl.title}</h3>
<p className="text-xs text-gray-500 mt-1 line-clamp-2">{ctrl.objective}</p>
{/* Open anchors summary */}
<div className="flex items-center gap-2 mt-2">
<BookOpen className="w-3 h-3 text-green-600" />
<span className="text-xs text-green-700">
{ctrl.open_anchors.length} Open-Source-Referenzen:
</span>
<span className="text-xs text-gray-500">
{ctrl.open_anchors.map(a => a.framework).filter((v, i, arr) => arr.indexOf(v) === i).join(', ')}
</span>
</div>
</div>
<ChevronRight className="w-4 h-4 text-gray-300 group-hover:text-purple-500 flex-shrink-0 mt-1 ml-4" />
</div>
</button>
))}
{filteredControls.length === 0 && (
<div className="text-center py-12 text-gray-400 text-sm">
{controls.length === 0
? 'Noch keine Controls vorhanden. Klicke auf "Neues Control" um zu starten.'
: 'Keine Controls gefunden.'}
</div>
)}
</div>
</div>
</div>
)
}