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 32s
CI/CD / test-python-backend-compliance (push) Successful in 34s
CI/CD / test-python-document-crawler (push) Successful in 23s
CI/CD / test-python-dsms-gateway (push) Successful in 21s
CI/CD / validate-canonical-controls (push) Successful in 11s
CI/CD / Deploy (push) Successful in 2s
Module 2: Extended Compliance Dashboard with roadmap, module-status, next-actions, snapshots, score-history Module 3: 7 German security document templates (IT-Sicherheitskonzept, Datenschutz, Backup, Logging, Incident-Response, Zugriff, Risikomanagement) Module 4: Compliance Process Manager with CRUD, complete/skip/seed, ~50 seed tasks, 3-tab UI Module 5: Evidence Collector Extended with automated checks, control-mapping, coverage report, 4-tab UI Also includes: canonical control library enhancements (verification method, categories, dedup), control generator improvements, RAG client extensions 52 tests pass, frontend builds clean. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
318 lines
16 KiB
TypeScript
318 lines
16 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { BookOpen, Trash2, Save, X } from 'lucide-react'
|
|
import { EMPTY_CONTROL, VERIFICATION_METHODS, CATEGORY_OPTIONS, TARGET_AUDIENCE_OPTIONS } from './helpers'
|
|
|
|
export 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>
|
|
|
|
{/* Verification Method, Category & Target Audience */}
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-600 mb-1">Nachweismethode</label>
|
|
<select
|
|
value={form.verification_method || ''}
|
|
onChange={e => setForm({ ...form, verification_method: e.target.value || null })}
|
|
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg"
|
|
>
|
|
<option value="">— Nicht zugewiesen —</option>
|
|
{Object.entries(VERIFICATION_METHODS).map(([k, v]) => (
|
|
<option key={k} value={k}>{v.label}</option>
|
|
))}
|
|
</select>
|
|
<p className="text-xs text-gray-400 mt-1">Wie wird dieses Control nachgewiesen?</p>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-600 mb-1">Kategorie</label>
|
|
<select
|
|
value={form.category || ''}
|
|
onChange={e => setForm({ ...form, category: e.target.value || null })}
|
|
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg"
|
|
>
|
|
<option value="">— Nicht zugewiesen —</option>
|
|
{CATEGORY_OPTIONS.map(c => (
|
|
<option key={c.value} value={c.value}>{c.label}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-600 mb-1">Zielgruppe</label>
|
|
<select
|
|
value={form.target_audience || ''}
|
|
onChange={e => setForm({ ...form, target_audience: e.target.value || null })}
|
|
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg"
|
|
>
|
|
<option value="">— Nicht zugewiesen —</option>
|
|
{Object.entries(TARGET_AUDIENCE_OPTIONS).map(([k, v]) => (
|
|
<option key={k} value={k}>{v.label}</option>
|
|
))}
|
|
</select>
|
|
<p className="text-xs text-gray-400 mt-1">Fuer wen ist dieses Control relevant?</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|