diff --git a/admin-compliance/app/sdk/control-library/__tests__/helpers.test.ts b/admin-compliance/app/sdk/control-library/__tests__/helpers.test.ts new file mode 100644 index 0000000..5a35b55 --- /dev/null +++ b/admin-compliance/app/sdk/control-library/__tests__/helpers.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect } from 'vitest' +import { getDomain, BACKEND_URL, EMPTY_CONTROL, DOMAIN_OPTIONS, COLLECTION_OPTIONS } from '../components/helpers' + +describe('getDomain', () => { + it('extracts domain from control_id', () => { + expect(getDomain('AUTH-001')).toBe('AUTH') + expect(getDomain('NET-042')).toBe('NET') + expect(getDomain('CRYPT-003')).toBe('CRYPT') + }) + + it('returns empty string for invalid control_id', () => { + expect(getDomain('')).toBe('') + expect(getDomain('NODASH')).toBe('NODASH') + }) +}) + +describe('BACKEND_URL', () => { + it('points to canonical API proxy', () => { + expect(BACKEND_URL).toBe('/api/sdk/v1/canonical') + }) +}) + +describe('EMPTY_CONTROL', () => { + it('has required fields with default values', () => { + expect(EMPTY_CONTROL.framework_id).toBe('bp_security_v1') + expect(EMPTY_CONTROL.severity).toBe('medium') + expect(EMPTY_CONTROL.release_state).toBe('draft') + expect(EMPTY_CONTROL.tags).toEqual([]) + expect(EMPTY_CONTROL.requirements).toEqual(['']) + expect(EMPTY_CONTROL.test_procedure).toEqual(['']) + expect(EMPTY_CONTROL.evidence).toEqual([{ type: '', description: '' }]) + expect(EMPTY_CONTROL.open_anchors).toEqual([{ framework: '', ref: '', url: '' }]) + }) +}) + +describe('DOMAIN_OPTIONS', () => { + it('contains expected domains', () => { + const values = DOMAIN_OPTIONS.map(d => d.value) + expect(values).toContain('AUTH') + expect(values).toContain('NET') + expect(values).toContain('CRYPT') + expect(values).toContain('AI') + expect(values).toContain('COMP') + expect(values.length).toBe(10) + }) +}) + +describe('COLLECTION_OPTIONS', () => { + it('contains expected collections', () => { + const values = COLLECTION_OPTIONS.map(c => c.value) + expect(values).toContain('bp_compliance_ce') + expect(values).toContain('bp_compliance_gesetze') + expect(values).toContain('bp_compliance_datenschutz') + expect(values.length).toBe(6) + }) +}) diff --git a/admin-compliance/app/sdk/control-library/components/ControlDetail.tsx b/admin-compliance/app/sdk/control-library/components/ControlDetail.tsx new file mode 100644 index 0000000..df8d9c1 --- /dev/null +++ b/admin-compliance/app/sdk/control-library/components/ControlDetail.tsx @@ -0,0 +1,270 @@ +'use client' + +import { + ArrowLeft, ExternalLink, BookOpen, Scale, FileText, + Eye, CheckCircle2, Trash2, Pencil, Clock, + ChevronLeft, SkipForward, +} from 'lucide-react' +import { CanonicalControl, EFFORT_LABELS, SeverityBadge, StateBadge, LicenseRuleBadge } from './helpers' + +interface ControlDetailProps { + ctrl: CanonicalControl + onBack: () => void + onEdit: () => void + onDelete: (controlId: string) => void + onReview: (controlId: string, action: string) => void + // Review mode navigation + reviewMode?: boolean + reviewIndex?: number + reviewTotal?: number + onReviewPrev?: () => void + onReviewNext?: () => void +} + +export function ControlDetail({ + ctrl, + onBack, + onEdit, + onDelete, + onReview, + reviewMode, + reviewIndex = 0, + reviewTotal = 0, + onReviewPrev, + onReviewNext, +}: ControlDetailProps) { + return ( +
+ {/* Header */} +
+
+ +
+
+ {ctrl.control_id} + + + +
+

{ctrl.title}

+
+
+
+ {reviewMode && ( +
+ + {reviewIndex + 1} / {reviewTotal} + +
+ )} + + +
+
+ + {/* Content */} +
+ {/* Objective */} +
+

Ziel

+

{ctrl.objective}

+
+ + {/* Rationale */} +
+

Begruendung

+

{ctrl.rationale}

+
+ + {/* Source Info (Rule 1 + 2) */} + {ctrl.source_citation && ( +
+
+ +

Quellenangabe

+
+
+ {Object.entries(ctrl.source_citation).map(([k, v]) => ( +

{k}: {v}

+ ))} +
+ {ctrl.source_original_text && ( +
+ Originaltext anzeigen +

+ {ctrl.source_original_text} +

+
+ )} +
+ )} + + {/* Scope */} + {(ctrl.scope.platforms?.length || ctrl.scope.components?.length || ctrl.scope.data_classes?.length) ? ( +
+

Geltungsbereich

+
+ {ctrl.scope.platforms?.length ? ( +
Plattformen: {ctrl.scope.platforms.join(', ')}
+ ) : null} + {ctrl.scope.components?.length ? ( +
Komponenten: {ctrl.scope.components.join(', ')}
+ ) : null} + {ctrl.scope.data_classes?.length ? ( +
Datenklassen: {ctrl.scope.data_classes.join(', ')}
+ ) : null} +
+
+ ) : null} + + {/* Requirements */} + {ctrl.requirements.length > 0 && ( +
+

Anforderungen

+
    + {ctrl.requirements.map((r, i) => ( +
  1. {r}
  2. + ))} +
+
+ )} + + {/* Test Procedure */} + {ctrl.test_procedure.length > 0 && ( +
+

Pruefverfahren

+
    + {ctrl.test_procedure.map((s, i) => ( +
  1. {s}
  2. + ))} +
+
+ )} + + {/* Evidence */} + {ctrl.evidence.length > 0 && ( +
+

Nachweise

+
+ {ctrl.evidence.map((ev, i) => ( +
+ +
{ev.type}: {ev.description}
+
+ ))} +
+
+ )} + + {/* Meta */} +
+ {ctrl.risk_score !== null &&
Risiko-Score: {ctrl.risk_score}
} + {ctrl.implementation_effort &&
Aufwand: {EFFORT_LABELS[ctrl.implementation_effort] || ctrl.implementation_effort}
} + {ctrl.tags.length > 0 && ( +
+ {ctrl.tags.map(t => ( + {t} + ))} +
+ )} +
+ + {/* Open Anchors */} +
+
+ +

Open-Source-Referenzen ({ctrl.open_anchors.length})

+
+ {ctrl.open_anchors.length > 0 ? ( +
+ {ctrl.open_anchors.map((anchor, i) => ( +
+ + {anchor.framework} + {anchor.ref} + {anchor.url && ( + + Link + + )} +
+ ))} +
+ ) : ( +

Keine Referenzen vorhanden.

+ )} +
+ + {/* Generation Metadata (internal) */} + {ctrl.generation_metadata && ( +
+
+ +

Generierungsdetails (intern)

+
+
+

Pfad: {String(ctrl.generation_metadata.processing_path || '-')}

+ {ctrl.generation_metadata.similarity_status && ( +

Similarity: {String(ctrl.generation_metadata.similarity_status)}

+ )} + {Array.isArray(ctrl.generation_metadata.similar_controls) && ( +
+

Aehnliche Controls:

+ {(ctrl.generation_metadata.similar_controls as Array>).map((s, i) => ( +

{String(s.control_id)} — {String(s.title)} ({String(s.similarity)})

+ ))} +
+ )} +
+
+ )} + + {/* Review Actions */} + {['needs_review', 'too_close', 'duplicate'].includes(ctrl.release_state) && ( +
+
+ +

Review erforderlich

+ {reviewMode && ( + Review-Modus aktiv + )} +
+
+ + + +
+
+ )} +
+
+ ) +} diff --git a/admin-compliance/app/sdk/control-library/components/ControlForm.tsx b/admin-compliance/app/sdk/control-library/components/ControlForm.tsx new file mode 100644 index 0000000..5c28a41 --- /dev/null +++ b/admin-compliance/app/sdk/control-library/components/ControlForm.tsx @@ -0,0 +1,272 @@ +'use client' + +import { useState } from 'react' +import { BookOpen, Trash2, Save, X } from 'lucide-react' +import { EMPTY_CONTROL } 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 ( +
+
+

+ {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 */} +
+ +