feat(canonical-controls): Canonical Control Library — rechtssichere Security Controls
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 40s
CI/CD / test-python-backend-compliance (push) Successful in 41s
CI/CD / test-python-document-crawler (push) Successful in 26s
CI/CD / test-python-dsms-gateway (push) Successful in 23s
CI/CD / validate-canonical-controls (push) Successful in 18s
CI/CD / deploy-hetzner (push) Successful in 2m26s

Eigenstaendig formulierte Security Controls mit unabhaengiger Taxonomie
und Open-Source-Verankerung (OWASP, NIST, ENISA). Keine BSI-Nomenklatur.

- Migration 044: 5 DB-Tabellen (frameworks, controls, sources, licenses, mappings)
- 10 Seed Controls mit 39 Open-Source-Referenzen
- License Gate: Quellen-Berechtigungspruefung (analysis/excerpt/embeddings/product)
- Too-Close-Detektor: 5 Metriken (exact-phrase, token-overlap, ngram, embedding, LCS)
- REST API: 8 Endpoints unter /v1/canonical/
- Go Loader mit Multi-Index (ID, domain, severity, framework)
- Frontend: Control Library Browser + Provenance Wiki
- CI/CD: validate-controls.py Job (schema, no-leak, open-anchors)
- 67 Tests (8 Go + 59 Python), alle PASS
- MkDocs Dokumentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-12 19:55:06 +01:00
parent 8442115e7c
commit 050f353192
20 changed files with 3935 additions and 0 deletions

View File

@@ -0,0 +1,123 @@
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
/**
* Proxy: GET /api/sdk/v1/canonical?endpoint=...
*
* Routes to backend canonical control endpoints:
* endpoint=frameworks → GET /api/v1/canonical/frameworks
* endpoint=controls → GET /api/v1/canonical/controls(?severity=...&domain=...)
* endpoint=control&id= → GET /api/v1/canonical/controls/{id}
* endpoint=sources → GET /api/v1/canonical/sources
* endpoint=licenses → GET /api/v1/canonical/licenses
*/
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const endpoint = searchParams.get('endpoint') || 'frameworks'
let backendPath: string
switch (endpoint) {
case 'frameworks':
backendPath = '/api/v1/canonical/frameworks'
break
case 'controls': {
const severity = searchParams.get('severity')
const domain = searchParams.get('domain')
const params = new URLSearchParams()
if (severity) params.set('severity', severity)
if (domain) params.set('domain', domain)
const qs = params.toString()
backendPath = `/api/v1/canonical/controls${qs ? `?${qs}` : ''}`
break
}
case 'control': {
const controlId = searchParams.get('id')
if (!controlId) {
return NextResponse.json({ error: 'Missing control id' }, { status: 400 })
}
backendPath = `/api/v1/canonical/controls/${encodeURIComponent(controlId)}`
break
}
case 'sources':
backendPath = '/api/v1/canonical/sources'
break
case 'licenses':
backendPath = '/api/v1/canonical/licenses'
break
default:
return NextResponse.json({ error: `Unknown endpoint: ${endpoint}` }, { status: 400 })
}
const response = await fetch(`${BACKEND_URL}${backendPath}`)
if (!response.ok) {
if (response.status === 404) {
return NextResponse.json(null, { status: 404 })
}
const errorText = await response.text()
return NextResponse.json(
{ error: 'Backend error', details: errorText },
{ status: response.status }
)
}
return NextResponse.json(await response.json())
} catch (error) {
console.error('Canonical control proxy error:', error)
return NextResponse.json(
{ error: 'Failed to connect to backend' },
{ status: 503 }
)
}
}
/**
* Proxy: POST /api/sdk/v1/canonical?endpoint=similarity-check&id=...
*
* Routes to: POST /api/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')
if (endpoint !== 'similarity-check' || !controlId) {
return NextResponse.json({ error: 'Invalid endpoint or missing id' }, { status: 400 })
}
const body = await request.json()
const response = await fetch(
`${BACKEND_URL}/api/v1/canonical/controls/${encodeURIComponent(controlId)}/similarity-check`,
{
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())
} catch (error) {
console.error('Similarity check proxy error:', error)
return NextResponse.json(
{ error: 'Failed to connect to backend' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,484 @@
'use client'
import { useState, useEffect, useMemo, useCallback } from 'react'
import {
Shield, Search, ChevronRight, ArrowLeft, ExternalLink,
Filter, AlertTriangle, CheckCircle2, Info, Lock,
FileText, BookOpen, Scale,
} 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'
// =============================================================================
// 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 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>('')
// Load data
useEffect(() => {
async function load() {
try {
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)
}
}
load()
}, [])
// 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])
const handleBack = useCallback(() => setSelectedControl(null), [])
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>
)
}
// =========================================================================
// DETAIL VIEW
// =========================================================================
if (selectedControl) {
const ctrl = selectedControl
return (
<div className="max-w-4xl mx-auto p-6">
<button onClick={handleBack} className="flex items-center gap-1 text-sm text-gray-500 hover:text-gray-700 mb-6">
<ArrowLeft className="w-4 h-4" /> Zurueck zur Uebersicht
</button>
{/* 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>
</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)}
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">
Keine Controls gefunden.
</div>
)}
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,496 @@
'use client'
import { useState, useEffect } from 'react'
import {
Shield, BookOpen, ExternalLink, CheckCircle2, AlertTriangle,
Lock, Scale, FileText, Eye, ArrowLeft,
} from 'lucide-react'
import Link from 'next/link'
// =============================================================================
// TYPES
// =============================================================================
interface LicenseInfo {
license_id: string
name: string
terms_url: string | null
commercial_use: string
ai_training_restriction: string | null
tdm_allowed_under_44b: string | null
deletion_required: boolean
notes: string | null
}
interface SourceInfo {
source_id: string
title: string
publisher: string
url: string | null
version_label: string | null
language: string
license_id: string
license_name: string
commercial_use: string
allowed_analysis: boolean
allowed_store_excerpt: boolean
allowed_ship_embeddings: boolean
allowed_ship_in_product: boolean
vault_retention_days: number
vault_access_tier: string
}
// =============================================================================
// STATIC PROVENANCE DOCUMENTATION
// =============================================================================
const PROVENANCE_SECTIONS = [
{
id: 'methodology',
title: 'Methodik der Control-Erstellung',
content: `## Unabhaengige Formulierung
Alle Controls in der Canonical Control Library wurden **eigenstaendig formuliert** und folgen einer
**unabhaengigen Taxonomie**. Es werden keine proprietaeren Bezeichner, Nummern oder Strukturen
aus geschuetzten Quellen uebernommen.
### Dreistufiger Prozess
1. **Offene Recherche** — Identifikation von Security-Anforderungen aus oeffentlichen, frei zugaenglichen
Frameworks (OWASP, NIST, ENISA). Jede Anforderung wird aus mindestens 2 unabhaengigen offenen Quellen belegt.
2. **Eigenstaendige Formulierung** — Jedes Control wird mit eigener Sprache, eigener Struktur und eigener
Taxonomie (z.B. AUTH-001, NET-001) verfasst. Kein Copy-Paste, keine Paraphrase geschuetzter Texte.
3. **Too-Close-Pruefung** — Automatisierte Aehnlichkeitspruefung gegen Quelltexte mit 5 Metriken
(Token Overlap, N-Gram Jaccard, Embedding Cosine, LCS Ratio, Exact-Phrase). Nur Controls mit
Status PASS oder WARN (+ Human Review) werden freigegeben.
### Rechtliche Grundlage
- **UrhG §44b** — Text & Data Mining erlaubt fuer Analyse; Kopien werden danach geloescht
- **UrhG §23** — Hinreichender Abstand zum Originalwerk durch eigene Formulierung
- **BSI Nutzungsbedingungen** — Kommerzielle Nutzung nur mit Zustimmung; wir nutzen BSI-Dokumente
ausschliesslich als Analysegrundlage, nicht im Produkt`,
},
{
id: 'taxonomy',
title: 'Unabhaengige Taxonomie',
content: `## Eigenes Klassifikationssystem
Die Canonical Control Library verwendet ein **eigenes Domain-Schema**, das sich bewusst von
proprietaeren Frameworks unterscheidet:
| Domain | Name | Abgrenzung |
|--------|------|------------|
| AUTH | Identity & Access Management | Eigene Struktur, nicht BSI O.Auth_* |
| NET | Network & Transport Security | Eigene Struktur, nicht BSI O.Netz_* |
| SUP | Software Supply Chain | NIST SSDF / SLSA-basiert |
| LOG | Security Operations & Logging | OWASP Logging Best Practices |
| WEB | Web Application Security | OWASP ASVS-basiert |
| DATA | Data Governance & Classification | NIST SP 800-60 basiert |
| CRYP | Cryptographic Operations | NIST SP 800-57 basiert |
| REL | Release & Change Governance | OWASP SAMM basiert |
### ID-Format
Control-IDs folgen dem Muster \`DOMAIN-NNN\` (z.B. AUTH-001, NET-002). Dieses Format ist
**nicht von BSI oder anderen proprietaeren Standards abgeleitet**, sondern folgt einem
allgemein ueblichen Nummerierungsschema.`,
},
{
id: 'open-sources',
title: 'Offene Referenzquellen',
content: `## Primaere offene Quellen
Alle Controls sind in mindestens einer der folgenden **frei zugaenglichen** Quellen verankert:
### OWASP (CC BY-SA 4.0 — kommerziell erlaubt)
- **ASVS** — Application Security Verification Standard v4.0.3
- **MASVS** — Mobile Application Security Verification Standard v2.1
- **Top 10** — OWASP Top 10 (2021)
- **Cheat Sheets** — OWASP Cheat Sheet Series
- **SAMM** — Software Assurance Maturity Model
### NIST (Public Domain — keine Einschraenkungen)
- **SP 800-53 Rev.5** — Security and Privacy Controls
- **SP 800-63B** — Digital Identity Guidelines (Authentication)
- **SP 800-57** — Key Management Recommendations
- **SP 800-52 Rev.2** — TLS Implementation Guidelines
- **SP 800-92** — Log Management Guide
- **SP 800-218 (SSDF)** — Secure Software Development Framework
- **SP 800-60** — Information Types to Security Categories
### ENISA (CC BY 4.0 — kommerziell erlaubt)
- Good Practices for IoT/Mobile Security
- Data Protection Engineering
- Algorithms, Key Sizes and Parameters Report
### Weitere offene Quellen
- **SLSA** (Supply-chain Levels for Software Artifacts) — Google Open Source
- **CIS Controls v8** (CC BY-NC-ND — nur fuer interne Analyse)`,
},
{
id: 'restricted-sources',
title: 'Geschuetzte Quellen — Nur interne Analyse',
content: `## Quellen mit eingeschraenkter Nutzung
Die folgenden Quellen werden **ausschliesslich intern zur Analyse** verwendet.
Kein Text, keine Struktur, keine Bezeichner aus diesen Quellen erscheinen im Produkt.
### BSI (Nutzungsbedingungen — kommerziell eingeschraenkt)
- TR-03161 Teil 1-3 (Mobile, Web, Hintergrunddienste)
- Nutzung: TDM unter UrhG §44b, Kopien werden geloescht
- Kein Shipping von Zitaten, Embeddings oder Strukturen
### ISO/IEC (Kostenpflichtig — kein Shipping)
- ISO 27001, ISO 27002
- Nutzung: Nur als Referenz fuer Mapping, kein Text im Produkt
### ETSI (Restriktiv — kein kommerzieller Gebrauch)
- Nutzung: Nur als Hintergrundwissen, kein direkter Einfluss
### Trennungsprinzip
| Ebene | Geschuetzte Quelle | Offene Quelle |
|-------|--------------------|---------------|
| Analyse | ✅ Darf gelesen werden | ✅ Darf gelesen werden |
| Inspiration | ✅ Darf Ideen liefern | ✅ Darf Ideen liefern |
| Formulierung | ❌ Keine Uebernahme | ✅ Darf zitiert werden |
| Struktur | ❌ Keine Uebernahme | ✅ Darf verwendet werden |
| Produkttext | ❌ Nicht erlaubt | ✅ Erlaubt |`,
},
{
id: 'validation',
title: 'Automatisierte Validierung',
content: `## CI/CD-Pruefungen
Jedes Control wird bei jedem Commit automatisch geprueft:
### 1. Schema-Validierung
- Alle Pflichtfelder vorhanden
- Control-ID Format: \`^[A-Z]{2,6}-[0-9]{3}$\`
- Severity: low, medium, high, critical
- Risk Score: 0-10
### 2. No-Leak Scanner
Regex-Pruefung gegen verbotene Muster in produktfaehigen Feldern:
- \`O.[A-Za-z]+_[0-9]+\` — BSI Objective-IDs
- \`TR-03161\` — Direkte BSI-TR-Referenzen
- \`BSI-TR-\` — BSI-spezifische Locators
- \`Anforderung [A-Z].[0-9]+\` — BSI-Anforderungsformat
### 3. Open Anchor Check
Jedes freigegebene Control muss mindestens 1 Open-Source-Referenz haben.
### 4. Too-Close Detektor (5 Metriken)
| Metrik | Warn | Fail | Beschreibung |
|--------|------|------|-------------|
| Exact Phrase | ≥8 Tokens | ≥12 Tokens | Laengste identische Token-Sequenz |
| Token Overlap | ≥0.20 | ≥0.30 | Jaccard-Aehnlichkeit der Token-Mengen |
| 3-Gram Jaccard | ≥0.10 | ≥0.18 | Zeichenketten-Aehnlichkeit |
| Embedding Cosine | ≥0.86 | ≥0.92 | Semantische Aehnlichkeit (bge-m3) |
| LCS Ratio | ≥0.35 | ≥0.50 | Longest Common Subsequence |
**Entscheidungslogik:**
- **PASS** — Kein Fail + max 1 Warn
- **WARN** — Max 2 Warn, kein Fail → Human Review erforderlich
- **FAIL** — Irgendein Fail → Blockiert, Umformulierung noetig`,
},
]
// =============================================================================
// PAGE
// =============================================================================
export default function ControlProvenancePage() {
const [licenses, setLicenses] = useState<LicenseInfo[]>([])
const [sources, setSources] = useState<SourceInfo[]>([])
const [activeSection, setActiveSection] = useState('methodology')
const [loading, setLoading] = useState(true)
useEffect(() => {
async function load() {
try {
const [licRes, srcRes] = await Promise.all([
fetch('/api/sdk/v1/canonical?endpoint=licenses'),
fetch('/api/sdk/v1/canonical?endpoint=sources'),
])
if (licRes.ok) setLicenses(await licRes.json())
if (srcRes.ok) setSources(await srcRes.json())
} catch {
// silently continue — static content still shown
} finally {
setLoading(false)
}
}
load()
}, [])
const currentSection = PROVENANCE_SECTIONS.find(s => s.id === activeSection)
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 gap-3">
<FileText className="w-6 h-6 text-green-600" />
<div>
<h1 className="text-lg font-semibold text-gray-900">Control Provenance Wiki</h1>
<p className="text-xs text-gray-500">
Dokumentation der unabhaengigen Herkunft aller Security Controls rechtssicherer Nachweis
</p>
</div>
<Link
href="/sdk/control-library"
className="ml-auto flex items-center gap-1 text-sm text-purple-600 hover:text-purple-800"
>
<Shield className="w-4 h-4" />
Zur Control Library
</Link>
</div>
</div>
<div className="flex flex-1 overflow-hidden">
{/* Left: Navigation */}
<div className="w-72 border-r border-gray-200 bg-gray-50 overflow-y-auto flex-shrink-0">
<div className="p-3 space-y-1">
<p className="text-xs font-semibold text-gray-400 uppercase px-3 mb-2">Dokumentation</p>
{PROVENANCE_SECTIONS.map(section => (
<button
key={section.id}
onClick={() => setActiveSection(section.id)}
className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-colors ${
activeSection === section.id
? 'bg-green-100 text-green-900 font-medium'
: 'text-gray-700 hover:bg-gray-100'
}`}
>
{section.title}
</button>
))}
<div className="border-t border-gray-200 mt-3 pt-3">
<p className="text-xs font-semibold text-gray-400 uppercase px-3 mb-2">Live-Daten</p>
<button
onClick={() => setActiveSection('license-matrix')}
className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-colors ${
activeSection === 'license-matrix'
? 'bg-green-100 text-green-900 font-medium'
: 'text-gray-700 hover:bg-gray-100'
}`}
>
Lizenz-Matrix
</button>
<button
onClick={() => setActiveSection('source-registry')}
className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-colors ${
activeSection === 'source-registry'
? 'bg-green-100 text-green-900 font-medium'
: 'text-gray-700 hover:bg-gray-100'
}`}
>
Quellenregister
</button>
</div>
</div>
</div>
{/* Right: Content */}
<div className="flex-1 overflow-y-auto p-6">
<div className="max-w-3xl mx-auto">
{/* Static documentation sections */}
{currentSection && (
<div>
<h2 className="text-xl font-bold text-gray-900 mb-4">{currentSection.title}</h2>
<div className="prose prose-sm max-w-none">
<MarkdownRenderer content={currentSection.content} />
</div>
</div>
)}
{/* License Matrix (live data) */}
{activeSection === 'license-matrix' && (
<div>
<h2 className="text-xl font-bold text-gray-900 mb-4">Lizenz-Matrix</h2>
<p className="text-sm text-gray-600 mb-4">
Uebersicht aller Lizenzen mit ihren erlaubten Nutzungsarten.
</p>
{loading ? (
<div className="animate-pulse h-32 bg-gray-100 rounded" />
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm border-collapse">
<thead>
<tr className="bg-gray-50">
<th className="text-left px-3 py-2 border-b font-medium text-gray-600">Lizenz</th>
<th className="text-left px-3 py-2 border-b font-medium text-gray-600">Kommerziell</th>
<th className="text-left px-3 py-2 border-b font-medium text-gray-600">AI-Training</th>
<th className="text-left px-3 py-2 border-b font-medium text-gray-600">TDM (§44b)</th>
<th className="text-left px-3 py-2 border-b font-medium text-gray-600">Loeschpflicht</th>
</tr>
</thead>
<tbody>
{licenses.map(lic => (
<tr key={lic.license_id} className="hover:bg-gray-50">
<td className="px-3 py-2 border-b">
<div className="font-medium text-gray-900">{lic.license_id}</div>
<div className="text-xs text-gray-500">{lic.name}</div>
</td>
<td className="px-3 py-2 border-b">
<UsageBadge value={lic.commercial_use} />
</td>
<td className="px-3 py-2 border-b">
<UsageBadge value={lic.ai_training_restriction || 'n/a'} />
</td>
<td className="px-3 py-2 border-b">
<UsageBadge value={lic.tdm_allowed_under_44b || 'unclear'} />
</td>
<td className="px-3 py-2 border-b">
{lic.deletion_required ? (
<span className="text-red-600 text-xs font-medium">Ja</span>
) : (
<span className="text-green-600 text-xs font-medium">Nein</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)}
{/* Source Registry (live data) */}
{activeSection === 'source-registry' && (
<div>
<h2 className="text-xl font-bold text-gray-900 mb-4">Quellenregister</h2>
<p className="text-sm text-gray-600 mb-4">
Alle registrierten Quellen mit ihren Berechtigungen.
</p>
{loading ? (
<div className="animate-pulse h-32 bg-gray-100 rounded" />
) : (
<div className="space-y-3">
{sources.map(src => (
<div key={src.source_id} className="bg-white border border-gray-200 rounded-lg p-4">
<div className="flex items-start justify-between mb-2">
<div>
<h3 className="text-sm font-medium text-gray-900">{src.title}</h3>
<p className="text-xs text-gray-500">{src.publisher} {src.license_name}</p>
</div>
{src.url && (
<a
href={src.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-xs text-blue-600 hover:text-blue-800"
>
<ExternalLink className="w-3 h-3" />
Quelle
</a>
)}
</div>
<div className="flex items-center gap-3 mt-2">
<PermBadge label="Analyse" allowed={src.allowed_analysis} />
<PermBadge label="Excerpt" allowed={src.allowed_store_excerpt} />
<PermBadge label="Embeddings" allowed={src.allowed_ship_embeddings} />
<PermBadge label="Produkt" allowed={src.allowed_ship_in_product} />
</div>
</div>
))}
</div>
)}
</div>
)}
</div>
</div>
</div>
</div>
)
}
// =============================================================================
// HELPER COMPONENTS
// =============================================================================
function UsageBadge({ value }: { value: string }) {
const config: Record<string, { bg: string; label: string }> = {
allowed: { bg: 'bg-green-100 text-green-800', label: 'Erlaubt' },
restricted: { bg: 'bg-yellow-100 text-yellow-800', label: 'Eingeschraenkt' },
prohibited: { bg: 'bg-red-100 text-red-800', label: 'Verboten' },
unclear: { bg: 'bg-gray-100 text-gray-600', label: 'Unklar' },
yes: { bg: 'bg-green-100 text-green-800', label: 'Ja' },
no: { bg: 'bg-red-100 text-red-800', label: 'Nein' },
'n/a': { bg: 'bg-gray-100 text-gray-400', label: 'k.A.' },
}
const c = config[value] || config.unclear
return <span className={`inline-flex px-1.5 py-0.5 rounded text-xs font-medium ${c.bg}`}>{c.label}</span>
}
function PermBadge({ label, allowed }: { label: string; allowed: boolean }) {
return (
<div className="flex items-center gap-1">
{allowed ? (
<CheckCircle2 className="w-3 h-3 text-green-500" />
) : (
<Lock className="w-3 h-3 text-red-400" />
)}
<span className={`text-xs ${allowed ? 'text-green-700' : 'text-red-500'}`}>{label}</span>
</div>
)
}
function MarkdownRenderer({ content }: { content: string }) {
let html = content
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
// Code blocks
html = html.replace(
/^```[\w]*\n([\s\S]*?)^```$/gm,
(_m, code: string) => `<pre class="bg-gray-50 border rounded p-3 my-3 text-xs font-mono overflow-x-auto whitespace-pre">${code.trimEnd()}</pre>`
)
// Tables
html = html.replace(
/^(\|.+\|)\n(\|[\s:|-]+\|)\n((?:\|.+\|\n?)*)/gm,
(_m, header: string, _sep: string, body: string) => {
const ths = header.split('|').filter((c: string) => c.trim()).map((c: string) =>
`<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase border-b">${c.trim()}</th>`
).join('')
const rows = body.trim().split('\n').map((row: string) => {
const tds = row.split('|').filter((c: string) => c.trim()).map((c: string) =>
`<td class="px-3 py-2 text-sm text-gray-700 border-b border-gray-100">${c.trim()}</td>`
).join('')
return `<tr>${tds}</tr>`
}).join('')
return `<table class="w-full border-collapse my-3 text-sm"><thead><tr>${ths}</tr></thead><tbody>${rows}</tbody></table>`
}
)
// Headers
html = html.replace(/^### (.+)$/gm, '<h4 class="text-sm font-semibold text-gray-800 mt-4 mb-2">$1</h4>')
html = html.replace(/^## (.+)$/gm, '<h3 class="text-base font-semibold text-gray-900 mt-5 mb-2">$1</h3>')
// Bold
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
// Inline code
html = html.replace(/`([^`]+)`/g, '<code class="bg-gray-100 px-1 py-0.5 rounded text-xs font-mono">$1</code>')
// Lists
html = html.replace(/^- (.+)$/gm, '<li class="ml-4 text-sm text-gray-700 list-disc">$1</li>')
html = html.replace(/((?:<li[^>]*>.*<\/li>\n?)+)/g, '<ul class="my-2 space-y-1">$1</ul>')
// Numbered lists
html = html.replace(/^(\d+)\. (.+)$/gm, '<li class="ml-4 text-sm text-gray-700 list-decimal">$2</li>')
// Paragraphs
html = html.replace(/^(?!<[hultdp]|$)(.+)$/gm, '<p class="text-sm text-gray-700 my-2">$1</p>')
return <div dangerouslySetInnerHTML={{ __html: html }} />
}

View File

@@ -906,6 +906,34 @@ export const SDK_STEPS: SDKStep[] = [
prerequisiteSteps: ['academy'],
isOptional: false,
},
{
id: 'control-library',
seq: 4900,
phase: 2,
package: 'betrieb',
order: 11,
name: 'Control Library',
nameShort: 'Controls',
description: 'Canonical Security Controls mit Open-Source-Referenzen',
url: '/sdk/control-library',
checkpointId: 'CP-CLIB',
prerequisiteSteps: [],
isOptional: true,
},
{
id: 'control-provenance',
seq: 4950,
phase: 2,
package: 'betrieb',
order: 12,
name: 'Control Provenance',
nameShort: 'Provenance',
description: 'Herkunftsnachweis: Offene Quellen, Lizenzen, Too-Close-Pruefung',
url: '/sdk/control-provenance',
checkpointId: 'CP-CPROV',
prerequisiteSteps: [],
isOptional: true,
},
]
// =============================================================================