feat(tom): audit document, compliance checks, 25 controls, canonical control mapping

Phase A: TOM document HTML generator (12 sections, inline CSS, A4 print)
Phase B: TOMDocumentTab component (org-header form, revisions, print/download)
Phase C: 11 compliance checks with severity-weighted scoring
Phase D: MkDocs documentation for TOM module
Phase E: 25 new controls (63 → 88) in 13 categories

Canonical Control Mapping (three-layer architecture):
- Migration 068: tom_control_mappings + tom_control_sync_state tables
- 6 API endpoints: sync, list, by-tom, stats, manual add, delete
- Category mapping: 13 TOM categories → 17 canonical categories
- Frontend: sync button + coverage card (Overview), drill-down (Editor),
  belegende Controls count (Document)
- 20 tests (unit + API with mocked DB)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-19 11:56:53 +01:00
parent 2a70441eaa
commit 4b1eede45b
14 changed files with 3910 additions and 8 deletions

View File

@@ -1,18 +1,19 @@
'use client'
import React, { useState, useCallback, useMemo } from 'react'
import React, { useState, useCallback, useMemo, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { useSDK } from '@/lib/sdk'
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
import { useTOMGenerator } from '@/lib/sdk/tom-generator/context'
import { DerivedTOM } from '@/lib/sdk/tom-generator/types'
import { TOMOverviewTab, TOMEditorTab, TOMGapExportTab } from '@/components/sdk/tom-dashboard'
import { TOMOverviewTab, TOMEditorTab, TOMGapExportTab, TOMDocumentTab } from '@/components/sdk/tom-dashboard'
import { runTOMComplianceCheck, type TOMComplianceCheckResult } from '@/lib/sdk/tom-compliance'
// =============================================================================
// TYPES
// =============================================================================
type Tab = 'uebersicht' | 'editor' | 'generator' | 'gap-export'
type Tab = 'uebersicht' | 'editor' | 'generator' | 'gap-export' | 'tom-dokument'
interface TabDefinition {
key: Tab
@@ -24,6 +25,7 @@ const TABS: TabDefinition[] = [
{ key: 'editor', label: 'Detail-Editor' },
{ key: 'generator', label: 'Generator' },
{ key: 'gap-export', label: 'Gap-Analyse & Export' },
{ key: 'tom-dokument', label: 'TOM-Dokument' },
]
// =============================================================================
@@ -41,6 +43,17 @@ export default function TOMPage() {
const [tab, setTab] = useState<Tab>('uebersicht')
const [selectedTOMId, setSelectedTOMId] = useState<string | null>(null)
const [complianceResult, setComplianceResult] = useState<TOMComplianceCheckResult | null>(null)
// ---------------------------------------------------------------------------
// Compliance check (auto-run when derivedTOMs change)
// ---------------------------------------------------------------------------
useEffect(() => {
if (state?.derivedTOMs && Array.isArray(state.derivedTOMs) && state.derivedTOMs.length > 0) {
setComplianceResult(runTOMComplianceCheck(state))
}
}, [state?.derivedTOMs])
// ---------------------------------------------------------------------------
// Computed / memoised values
@@ -316,6 +329,17 @@ export default function TOMPage() {
/>
)
// ---------------------------------------------------------------------------
// Tab 5 TOM-Dokument
// ---------------------------------------------------------------------------
const renderTOMDokument = () => (
<TOMDocumentTab
state={state}
complianceResult={complianceResult}
/>
)
// ---------------------------------------------------------------------------
// Tab content router
// ---------------------------------------------------------------------------
@@ -330,6 +354,8 @@ export default function TOMPage() {
return renderGenerator()
case 'gap-export':
return renderGapExport()
case 'tom-dokument':
return renderTOMDokument()
default:
return renderUebersicht()
}

View File

@@ -0,0 +1,449 @@
'use client'
import { useState, useEffect, useCallback, useMemo } from 'react'
import type { TOMGeneratorState } from '@/lib/sdk/tom-generator/types'
import type { TOMComplianceCheckResult } from '@/lib/sdk/tom-compliance'
import {
buildTOMDocumentHtml,
createDefaultTOMDocumentOrgHeader,
type TOMDocumentOrgHeader,
type TOMDocumentRevision,
} from '@/lib/sdk/tom-document'
// =============================================================================
// TYPES
// =============================================================================
interface TOMDocumentTabProps {
state: TOMGeneratorState
complianceResult: TOMComplianceCheckResult | null
}
// =============================================================================
// COMPONENT
// =============================================================================
function TOMDocumentTab({ state, complianceResult }: TOMDocumentTabProps) {
// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------
const [orgHeader, setOrgHeader] = useState<TOMDocumentOrgHeader>(() => {
if (typeof window !== 'undefined') {
const saved = localStorage.getItem('bp_tom_document_orgheader')
if (saved) {
try { return JSON.parse(saved) } catch { /* ignore */ }
}
}
return createDefaultTOMDocumentOrgHeader()
})
const [revisions, setRevisions] = useState<TOMDocumentRevision[]>(() => {
if (typeof window !== 'undefined') {
const saved = localStorage.getItem('bp_tom_document_revisions')
if (saved) {
try { return JSON.parse(saved) } catch { /* ignore */ }
}
}
return []
})
// ---------------------------------------------------------------------------
// localStorage persistence
// ---------------------------------------------------------------------------
useEffect(() => {
localStorage.setItem('bp_tom_document_orgheader', JSON.stringify(orgHeader))
}, [orgHeader])
useEffect(() => {
localStorage.setItem('bp_tom_document_revisions', JSON.stringify(revisions))
}, [revisions])
// ---------------------------------------------------------------------------
// Computed values
// ---------------------------------------------------------------------------
const tomCount = useMemo(() => {
if (!state?.derivedTOMs) return 0
return Array.isArray(state.derivedTOMs) ? state.derivedTOMs.length : 0
}, [state?.derivedTOMs])
const applicableTOMs = useMemo(() => {
if (!state?.derivedTOMs || !Array.isArray(state.derivedTOMs)) return []
return state.derivedTOMs.filter(t => t.applicability !== 'NOT_APPLICABLE')
}, [state?.derivedTOMs])
const implementedCount = useMemo(() => {
return applicableTOMs.filter(t => t.implementationStatus === 'IMPLEMENTED').length
}, [applicableTOMs])
const [canonicalCount, setCanonicalCount] = useState(0)
useEffect(() => {
if (tomCount === 0) return
fetch('/api/sdk/v1/compliance/tom-mappings/stats')
.then(r => r.ok ? r.json() : null)
.then(data => { if (data?.sync_state?.canonical_controls_matched) setCanonicalCount(data.sync_state.canonical_controls_matched) })
.catch(() => {})
}, [tomCount])
// ---------------------------------------------------------------------------
// Handlers
// ---------------------------------------------------------------------------
const handlePrintTOMDocument = useCallback(() => {
const html = buildTOMDocumentHtml(
state?.derivedTOMs || [],
orgHeader,
state?.companyProfile || null,
state?.riskProfile || null,
complianceResult,
revisions,
)
const printWindow = window.open('', '_blank')
if (printWindow) {
printWindow.document.write(html)
printWindow.document.close()
setTimeout(() => printWindow.print(), 300)
}
}, [state, orgHeader, complianceResult, revisions])
const handleDownloadTOMDocumentHtml = useCallback(() => {
const html = buildTOMDocumentHtml(
state?.derivedTOMs || [],
orgHeader,
state?.companyProfile || null,
state?.riskProfile || null,
complianceResult,
revisions,
)
const blob = new Blob([html], { type: 'text/html;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `TOM-Dokumentation-${orgHeader.organizationName || 'Organisation'}-${new Date().toISOString().split('T')[0]}.html`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, [state, orgHeader, complianceResult, revisions])
const handleAddRevision = useCallback(() => {
setRevisions(prev => [...prev, {
version: String(prev.length + 1) + '.0',
date: new Date().toISOString().split('T')[0],
author: '',
changes: '',
}])
}, [])
const handleUpdateRevision = useCallback((index: number, field: keyof TOMDocumentRevision, value: string) => {
setRevisions(prev => prev.map((r, i) => i === index ? { ...r, [field]: value } : r))
}, [])
const handleRemoveRevision = useCallback((index: number) => {
setRevisions(prev => prev.filter((_, i) => i !== index))
}, [])
const updateOrgHeader = useCallback((field: keyof TOMDocumentOrgHeader, value: string | string[]) => {
setOrgHeader(prev => ({ ...prev, [field]: value }))
}, [])
// ---------------------------------------------------------------------------
// Render
// ---------------------------------------------------------------------------
return (
<div className="space-y-6">
{/* 1. Action Bar */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-gray-900">TOM-Dokument (Art. 32 DSGVO)</h3>
<p className="text-sm text-gray-500 mt-1">
Auditfaehiges Dokument mit {applicableTOMs.length} Massnahmen generieren
</p>
</div>
<div className="flex gap-3">
<button
onClick={handleDownloadTOMDocumentHtml}
disabled={tomCount === 0}
className="bg-white border border-gray-300 text-gray-700 hover:bg-gray-50 rounded-lg px-4 py-2 text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
HTML herunterladen
</button>
<button
onClick={handlePrintTOMDocument}
disabled={tomCount === 0}
className="bg-purple-600 text-white hover:bg-purple-700 rounded-lg px-4 py-2 text-sm font-medium transition-colors shadow-sm disabled:opacity-50 disabled:cursor-not-allowed"
>
Als PDF drucken
</button>
</div>
</div>
</div>
{/* 2. Org Header Form */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h4 className="text-base font-semibold text-gray-900 mb-4">Organisationsdaten</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Organisation</label>
<input
type="text"
value={orgHeader.organizationName}
onChange={e => updateOrgHeader('organizationName', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Branche</label>
<input
type="text"
value={orgHeader.industry}
onChange={e => updateOrgHeader('industry', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Datenschutzbeauftragter</label>
<input
type="text"
value={orgHeader.dpoName}
onChange={e => updateOrgHeader('dpoName', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">DSB-Kontakt</label>
<input
type="text"
value={orgHeader.dpoContact}
onChange={e => updateOrgHeader('dpoContact', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Verantwortlicher</label>
<input
type="text"
value={orgHeader.responsiblePerson}
onChange={e => updateOrgHeader('responsiblePerson', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">IT-Sicherheitskontakt</label>
<input
type="text"
value={orgHeader.itSecurityContact}
onChange={e => updateOrgHeader('itSecurityContact', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Mitarbeiteranzahl</label>
<input
type="text"
value={orgHeader.employeeCount}
onChange={e => updateOrgHeader('employeeCount', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Standorte</label>
<input
type="text"
value={orgHeader.locations.join(', ')}
onChange={e => updateOrgHeader('locations', e.target.value.split(',').map(s => s.trim()))}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Dokumentversion</label>
<input
type="text"
value={orgHeader.documentVersion}
onChange={e => updateOrgHeader('documentVersion', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Pruefintervall</label>
<input
type="text"
value={orgHeader.reviewInterval}
onChange={e => updateOrgHeader('reviewInterval', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Letzte Pruefung</label>
<input
type="date"
value={orgHeader.lastReviewDate}
onChange={e => updateOrgHeader('lastReviewDate', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Naechste Pruefung</label>
<input
type="date"
value={orgHeader.nextReviewDate}
onChange={e => updateOrgHeader('nextReviewDate', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</div>
</div>
</div>
{/* 3. Revisions Manager */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<h4 className="text-base font-semibold text-gray-900">Aenderungshistorie</h4>
<button
onClick={handleAddRevision}
className="text-sm text-purple-600 hover:text-purple-700 font-medium"
>
+ Version hinzufuegen
</button>
</div>
{revisions.length > 0 ? (
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-200">
<th className="text-left py-2 text-gray-600 font-medium">Version</th>
<th className="text-left py-2 text-gray-600 font-medium">Datum</th>
<th className="text-left py-2 text-gray-600 font-medium">Autor</th>
<th className="text-left py-2 text-gray-600 font-medium">Aenderungen</th>
<th className="py-2"></th>
</tr>
</thead>
<tbody>
{revisions.map((revision, index) => (
<tr key={index} className="border-b border-gray-100">
<td className="py-2 pr-2">
<input
type="text"
value={revision.version}
onChange={e => handleUpdateRevision(index, 'version', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-1.5 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</td>
<td className="py-2 pr-2">
<input
type="date"
value={revision.date}
onChange={e => handleUpdateRevision(index, 'date', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-1.5 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</td>
<td className="py-2 pr-2">
<input
type="text"
value={revision.author}
onChange={e => handleUpdateRevision(index, 'author', e.target.value)}
placeholder="Name"
className="w-full rounded-lg border border-gray-300 px-3 py-1.5 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</td>
<td className="py-2 pr-2">
<input
type="text"
value={revision.changes}
onChange={e => handleUpdateRevision(index, 'changes', e.target.value)}
placeholder="Beschreibung der Aenderungen"
className="w-full rounded-lg border border-gray-300 px-3 py-1.5 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</td>
<td className="py-2 text-right">
<button
onClick={() => handleRemoveRevision(index)}
className="text-sm text-red-600 hover:text-red-700 font-medium"
>
Entfernen
</button>
</td>
</tr>
))}
</tbody>
</table>
) : (
<p className="text-sm text-gray-500 italic">
Noch keine Revisionen. Die erste Version wird automatisch im Dokument eingetragen.
</p>
)}
</div>
{/* 4. Document Preview */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h4 className="text-base font-semibold text-gray-900 mb-4">Dokument-Vorschau</h4>
{tomCount === 0 ? (
<div className="text-center py-8">
<p className="text-gray-500">Starten Sie den TOM-Generator, um Massnahmen abzuleiten.</p>
</div>
) : (
<div className="space-y-4">
{/* Cover preview */}
<div className="bg-purple-50 rounded-lg p-4 text-center">
<p className="text-purple-700 font-semibold text-lg">TOM-Dokumentation</p>
<p className="text-purple-600 text-sm">
Art. 32 DSGVO {orgHeader.organizationName || 'Organisation'}
</p>
<p className="text-purple-500 text-xs mt-1">
Version {orgHeader.documentVersion} | Stand: {new Date().toLocaleDateString('de-DE')}
</p>
</div>
{/* Stats */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
<div className="bg-gray-50 rounded-lg p-3 text-center">
<p className="text-2xl font-bold text-gray-900">{applicableTOMs.length}</p>
<p className="text-xs text-gray-500">Massnahmen</p>
</div>
<div className="bg-green-50 rounded-lg p-3 text-center">
<p className="text-2xl font-bold text-green-700">{implementedCount}</p>
<p className="text-xs text-gray-500">Umgesetzt</p>
</div>
<div className="bg-purple-50 rounded-lg p-3 text-center">
<p className="text-2xl font-bold text-purple-700">{canonicalCount || '-'}</p>
<p className="text-xs text-gray-500">Belegende Controls</p>
</div>
<div className="bg-gray-50 rounded-lg p-3 text-center">
<p className="text-2xl font-bold text-gray-900">12</p>
<p className="text-xs text-gray-500">Sektionen</p>
</div>
<div className="bg-gray-50 rounded-lg p-3 text-center">
<p className="text-2xl font-bold text-gray-900">
{complianceResult ? complianceResult.score : '-'}
</p>
<p className="text-xs text-gray-500">Compliance-Score</p>
</div>
</div>
{/* 12 Sections list */}
<div>
<p className="text-sm font-medium text-gray-700 mb-2">12 Dokument-Sektionen:</p>
<ol className="list-decimal list-inside text-sm text-gray-600 space-y-1">
<li>Ziel und Zweck</li>
<li>Geltungsbereich</li>
<li>Grundprinzipien Art. 32</li>
<li>Schutzbedarf und Risikoanalyse</li>
<li>Massnahmen-Uebersicht</li>
<li>Detaillierte Massnahmen</li>
<li>SDM Gewaehrleistungsziele</li>
<li>Verantwortlichkeiten</li>
<li>Pruef- und Revisionszyklus</li>
<li>Compliance-Status</li>
<li>Aenderungshistorie</li>
</ol>
</div>
</div>
)}
</div>
</div>
)
}
export { TOMDocumentTab }

View File

@@ -1,9 +1,18 @@
'use client'
import { useMemo, useState, useEffect } from 'react'
import { useMemo, useState, useEffect, useCallback } from 'react'
import { DerivedTOM, TOMGeneratorState } from '@/lib/sdk/tom-generator/types'
import { getControlById } from '@/lib/sdk/tom-generator/controls/loader'
interface CanonicalMapping {
id: string
canonical_control_code: string
canonical_title: string | null
canonical_severity: string | null
canonical_objective: string | null
mapping_type: string
}
interface TOMEditorTabProps {
state: TOMGeneratorState
selectedTOMId: string | null
@@ -46,6 +55,17 @@ export function TOMEditorTab({ state, selectedTOMId, onUpdateTOM, onBack }: TOME
const [notes, setNotes] = useState('')
const [linkedEvidence, setLinkedEvidence] = useState<string[]>([])
const [selectedEvidenceId, setSelectedEvidenceId] = useState('')
const [canonicalMappings, setCanonicalMappings] = useState<CanonicalMapping[]>([])
const [showCanonical, setShowCanonical] = useState(false)
// Load canonical controls for this TOM's category
useEffect(() => {
if (!control?.category) { setCanonicalMappings([]); return }
fetch(`/api/sdk/v1/compliance/tom-mappings/by-tom/${encodeURIComponent(control.category)}`)
.then(r => r.ok ? r.json() : null)
.then(data => { if (data?.mappings) setCanonicalMappings(data.mappings) })
.catch(() => setCanonicalMappings([]))
}, [control?.category])
useEffect(() => {
if (tom) {
@@ -341,6 +361,62 @@ export function TOMEditorTab({ state, selectedTOMId, onUpdateTOM, onBack }: TOME
</div>
)}
{/* Canonical Controls (Belegende Security-Controls) */}
{canonicalMappings.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-gray-700">
Belegende Security-Controls ({canonicalMappings.length})
</h3>
<button
onClick={() => setShowCanonical(!showCanonical)}
className="text-xs text-purple-600 hover:text-purple-700 font-medium"
>
{showCanonical ? 'Einklappen' : 'Alle anzeigen'}
</button>
</div>
<div className="space-y-2">
{(showCanonical ? canonicalMappings : canonicalMappings.slice(0, 5)).map(m => (
<div key={m.id} className="flex items-start gap-3 bg-gray-50 rounded-lg px-3 py-2">
<div className="flex-shrink-0">
<span className="text-xs font-mono bg-purple-100 text-purple-700 px-1.5 py-0.5 rounded">
{m.canonical_control_code}
</span>
</div>
<div className="flex-1 min-w-0">
<p className="text-sm text-gray-700 font-medium truncate">{m.canonical_title || m.canonical_control_code}</p>
{m.canonical_objective && showCanonical && (
<p className="text-xs text-gray-500 mt-0.5 line-clamp-2">{m.canonical_objective}</p>
)}
</div>
<div className="flex-shrink-0 flex items-center gap-1.5">
{m.canonical_severity && (
<span className={`text-xs px-1.5 py-0.5 rounded-full font-medium ${
m.canonical_severity === 'critical' ? 'bg-red-100 text-red-700' :
m.canonical_severity === 'high' ? 'bg-orange-100 text-orange-700' :
m.canonical_severity === 'medium' ? 'bg-yellow-100 text-yellow-700' :
'bg-gray-100 text-gray-600'
}`}>
{m.canonical_severity}
</span>
)}
<span className={`text-xs px-1.5 py-0.5 rounded-full ${
m.mapping_type === 'manual' ? 'bg-blue-100 text-blue-600' : 'bg-gray-100 text-gray-500'
}`}>
{m.mapping_type === 'manual' ? 'manuell' : 'auto'}
</span>
</div>
</div>
))}
</div>
{!showCanonical && canonicalMappings.length > 5 && (
<p className="text-xs text-gray-400 mt-2">
+ {canonicalMappings.length - 5} weitere Controls
</p>
)}
</div>
)}
{/* Framework Mappings */}
{control?.mappings && control.mappings.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">

View File

@@ -1,6 +1,6 @@
'use client'
import { useMemo, useState } from 'react'
import { useMemo, useState, useEffect, useCallback } from 'react'
import { DerivedTOM, TOMGeneratorState } from '@/lib/sdk/tom-generator/types'
import { getControlById, getControlsByCategory, getAllCategories } from '@/lib/sdk/tom-generator/controls/loader'
import { SDM_GOAL_LABELS, getSDMCoverageStats, SDMGewaehrleistungsziel } from '@/lib/sdk/tom-generator/sdm-mapping'
@@ -11,6 +11,18 @@ interface TOMOverviewTabProps {
onStartGenerator: () => void
}
interface MappingStats {
sync_state: {
profile_hash: string | null
total_mappings: number
canonical_controls_matched: number
tom_controls_covered: number
last_synced_at: string | null
}
category_breakdown: { tom_category: string; total_mappings: number; unique_controls: number }[]
total_canonical_controls_available: number
}
const STATUS_BADGES: Record<string, { label: string; className: string }> = {
IMPLEMENTED: { label: 'Implementiert', className: 'bg-green-100 text-green-700' },
PARTIAL: { label: 'Teilweise', className: 'bg-yellow-100 text-yellow-700' },
@@ -34,9 +46,41 @@ export function TOMOverviewTab({ state, onSelectTOM, onStartGenerator }: TOMOver
const [typeFilter, setTypeFilter] = useState<string>('ALL')
const [statusFilter, setStatusFilter] = useState<string>('ALL')
const [applicabilityFilter, setApplicabilityFilter] = useState<string>('ALL')
const [mappingStats, setMappingStats] = useState<MappingStats | null>(null)
const [syncing, setSyncing] = useState(false)
const categories = useMemo(() => getAllCategories(), [])
// Load mapping stats
useEffect(() => {
if (state.derivedTOMs.length === 0) return
fetch('/api/sdk/v1/compliance/tom-mappings/stats')
.then(r => r.ok ? r.json() : null)
.then(data => { if (data) setMappingStats(data) })
.catch(() => {})
}, [state.derivedTOMs.length])
const handleSyncControls = useCallback(async () => {
setSyncing(true)
try {
const resp = await fetch('/api/sdk/v1/compliance/tom-mappings/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
industry: state.companyProfile?.industry || null,
company_size: state.companyProfile?.size || null,
force: false,
}),
})
if (resp.ok) {
// Reload stats after sync
const statsResp = await fetch('/api/sdk/v1/compliance/tom-mappings/stats')
if (statsResp.ok) setMappingStats(await statsResp.json())
}
} catch { /* ignore */ }
setSyncing(false)
}, [state.companyProfile])
const stats = useMemo(() => {
const toms = state.derivedTOMs
return {
@@ -159,6 +203,59 @@ export function TOMOverviewTab({ state, onSelectTOM, onStartGenerator }: TOMOver
</div>
</div>
{/* Canonical Control Library Coverage */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-center justify-between mb-3">
<div>
<h3 className="text-sm font-semibold text-gray-700">Canonical Control Library</h3>
<p className="text-xs text-gray-500 mt-0.5">
Belegende Security-Controls aus OWASP, NIST, ENISA
</p>
</div>
<button
onClick={handleSyncControls}
disabled={syncing}
className="bg-purple-600 text-white hover:bg-purple-700 disabled:bg-gray-300 rounded-lg px-4 py-2 text-xs font-medium transition-colors"
>
{syncing ? 'Synchronisiere...' : 'Controls synchronisieren'}
</button>
</div>
{mappingStats ? (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
<div className="bg-gray-50 rounded-lg p-3 text-center">
<div className="text-xl font-bold text-gray-900">{mappingStats.sync_state.total_mappings}</div>
<div className="text-xs text-gray-500">Zugeordnete Controls</div>
</div>
<div className="bg-gray-50 rounded-lg p-3 text-center">
<div className="text-xl font-bold text-purple-600">{mappingStats.sync_state.canonical_controls_matched}</div>
<div className="text-xs text-gray-500">Einzigartige Controls</div>
</div>
<div className="bg-gray-50 rounded-lg p-3 text-center">
<div className="text-xl font-bold text-gray-900">{mappingStats.sync_state.tom_controls_covered}/13</div>
<div className="text-xs text-gray-500">Kategorien abgedeckt</div>
</div>
<div className="bg-gray-50 rounded-lg p-3 text-center">
<div className="text-xl font-bold text-gray-900">{mappingStats.total_canonical_controls_available}</div>
<div className="text-xs text-gray-500">Verfuegbare Controls</div>
</div>
</div>
) : (
<div className="text-center py-4">
<p className="text-sm text-gray-400">
Noch keine Controls synchronisiert. Klicken Sie &quot;Controls synchronisieren&quot;, um relevante
Security-Controls aus der Canonical Control Library zuzuordnen.
</p>
</div>
)}
{mappingStats?.sync_state?.last_synced_at && (
<p className="text-xs text-gray-400 mt-2">
Letzte Synchronisation: {new Date(mappingStats.sync_state.last_synced_at).toLocaleDateString('de-DE', {
day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit'
})}
</p>
)}
</div>
{/* Filter Controls */}
<div className="bg-white rounded-xl border border-gray-200 p-4">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">

View File

@@ -1,3 +1,4 @@
export { TOMOverviewTab } from './TOMOverviewTab'
export { TOMEditorTab } from './TOMEditorTab'
export { TOMGapExportTab } from './TOMGapExportTab'
export { TOMDocumentTab } from './TOMDocumentTab'

View File

@@ -0,0 +1,553 @@
// =============================================================================
// TOM Module - Compliance Check Engine
// Prueft Technische und Organisatorische Massnahmen auf Vollstaendigkeit,
// Konsistenz und DSGVO-Konformitaet (Art. 32 DSGVO)
// =============================================================================
import type {
TOMGeneratorState,
DerivedTOM,
RiskProfile,
DataProfile,
ControlCategory,
ImplementationStatus,
} from './tom-generator/types'
import { getControlById, getControlsByCategory, getAllCategories } from './tom-generator/controls/loader'
import { SDM_CATEGORY_MAPPING } from './tom-generator/types'
// =============================================================================
// TYPES
// =============================================================================
export type TOMComplianceIssueSeverity = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'
export type TOMComplianceIssueType =
| 'MISSING_RESPONSIBLE'
| 'OVERDUE_REVIEW'
| 'MISSING_EVIDENCE'
| 'INCOMPLETE_CATEGORY'
| 'NO_ENCRYPTION_MEASURES'
| 'NO_PSEUDONYMIZATION'
| 'MISSING_AVAILABILITY'
| 'NO_REVIEW_PROCESS'
| 'UNCOVERED_SDM_GOAL'
| 'HIGH_RISK_WITHOUT_MEASURES'
| 'STALE_NOT_IMPLEMENTED'
export interface TOMComplianceIssue {
id: string
controlId: string
controlName: string
type: TOMComplianceIssueType
severity: TOMComplianceIssueSeverity
title: string
description: string
recommendation: string
}
export interface TOMComplianceCheckResult {
issues: TOMComplianceIssue[]
score: number // 0-100
stats: {
total: number
passed: number
failed: number
bySeverity: Record<TOMComplianceIssueSeverity, number>
}
}
// =============================================================================
// CONSTANTS
// =============================================================================
export const TOM_SEVERITY_LABELS_DE: Record<TOMComplianceIssueSeverity, string> = {
CRITICAL: 'Kritisch',
HIGH: 'Hoch',
MEDIUM: 'Mittel',
LOW: 'Niedrig',
}
export const TOM_SEVERITY_COLORS: Record<TOMComplianceIssueSeverity, string> = {
CRITICAL: '#dc2626',
HIGH: '#ea580c',
MEDIUM: '#d97706',
LOW: '#6b7280',
}
// =============================================================================
// HELPERS
// =============================================================================
let issueCounter = 0
function createIssueId(): string {
issueCounter++
return `TCI-${Date.now()}-${String(issueCounter).padStart(4, '0')}`
}
function createIssue(
controlId: string,
controlName: string,
type: TOMComplianceIssueType,
severity: TOMComplianceIssueSeverity,
title: string,
description: string,
recommendation: string
): TOMComplianceIssue {
return { id: createIssueId(), controlId, controlName, type, severity, title, description, recommendation }
}
function daysBetween(date: Date, now: Date): number {
const diffMs = now.getTime() - date.getTime()
return Math.floor(diffMs / (1000 * 60 * 60 * 24))
}
// =============================================================================
// PER-TOM CHECKS (1-3, 11)
// =============================================================================
/**
* Check 1: MISSING_RESPONSIBLE (MEDIUM)
* REQUIRED TOM without responsiblePerson AND responsibleDepartment.
*/
function checkMissingResponsible(tom: DerivedTOM): TOMComplianceIssue | null {
if (tom.applicability !== 'REQUIRED') return null
if (!tom.responsiblePerson && !tom.responsibleDepartment) {
return createIssue(
tom.controlId,
tom.name,
'MISSING_RESPONSIBLE',
'MEDIUM',
'Keine verantwortliche Person/Abteilung',
`Die TOM "${tom.name}" ist als REQUIRED eingestuft, hat aber weder eine verantwortliche Person noch eine verantwortliche Abteilung zugewiesen. Ohne klare Verantwortlichkeit kann die Massnahme nicht zuverlaessig umgesetzt und gepflegt werden.`,
'Weisen Sie eine verantwortliche Person oder Abteilung zu, die fuer die Umsetzung und regelmaessige Pruefung dieser Massnahme zustaendig ist.'
)
}
return null
}
/**
* Check 2: OVERDUE_REVIEW (MEDIUM)
* TOM with reviewDate in the past.
*/
function checkOverdueReview(tom: DerivedTOM): TOMComplianceIssue | null {
if (!tom.reviewDate) return null
const reviewDate = new Date(tom.reviewDate)
const now = new Date()
if (reviewDate < now) {
const overdueDays = daysBetween(reviewDate, now)
return createIssue(
tom.controlId,
tom.name,
'OVERDUE_REVIEW',
'MEDIUM',
'Ueberfaellige Pruefung',
`Die TOM "${tom.name}" haette am ${reviewDate.toLocaleDateString('de-DE')} geprueft werden muessen. Die Pruefung ist ${overdueDays} Tag(e) ueberfaellig. Gemaess Art. 32 Abs. 1 lit. d DSGVO ist eine regelmaessige Ueberpruefung der Wirksamkeit von TOMs erforderlich.`,
'Fuehren Sie umgehend eine Wirksamkeitspruefung dieser Massnahme durch und aktualisieren Sie das naechste Pruefungsdatum.'
)
}
return null
}
/**
* Check 3: MISSING_EVIDENCE (HIGH)
* IMPLEMENTED TOM where linkedEvidence is empty but the control has evidenceRequirements.
*/
function checkMissingEvidence(tom: DerivedTOM): TOMComplianceIssue | null {
if (tom.implementationStatus !== 'IMPLEMENTED') return null
if (tom.linkedEvidence.length > 0) return null
const control = getControlById(tom.controlId)
if (!control || control.evidenceRequirements.length === 0) return null
return createIssue(
tom.controlId,
tom.name,
'MISSING_EVIDENCE',
'HIGH',
'Kein Nachweis hinterlegt',
`Die TOM "${tom.name}" ist als IMPLEMENTED markiert, hat aber keine verknuepften Nachweisdokumente. Der Control erfordert ${control.evidenceRequirements.length} Nachweis(e): ${control.evidenceRequirements.join(', ')}. Ohne Nachweise ist die Umsetzung nicht auditfaehig.`,
'Laden Sie die erforderlichen Nachweisdokumente hoch und verknuepfen Sie sie mit dieser Massnahme.'
)
}
/**
* Check 11: STALE_NOT_IMPLEMENTED (LOW)
* REQUIRED TOM that has been NOT_IMPLEMENTED for >90 days.
* Uses implementationDate === null and state.createdAt / state.updatedAt as reference.
*/
function checkStaleNotImplemented(tom: DerivedTOM, state: TOMGeneratorState): TOMComplianceIssue | null {
if (tom.applicability !== 'REQUIRED') return null
if (tom.implementationStatus !== 'NOT_IMPLEMENTED') return null
if (tom.implementationDate !== null) return null
const referenceDate = state.createdAt ? new Date(state.createdAt) : (state.updatedAt ? new Date(state.updatedAt) : null)
if (!referenceDate) return null
const ageInDays = daysBetween(referenceDate, new Date())
if (ageInDays <= 90) return null
return createIssue(
tom.controlId,
tom.name,
'STALE_NOT_IMPLEMENTED',
'LOW',
'Langfristig nicht umgesetzte Pflichtmassnahme',
`Die TOM "${tom.name}" ist als REQUIRED eingestuft, aber seit ${ageInDays} Tagen nicht umgesetzt. Pflichtmassnahmen, die laenger als 90 Tage nicht implementiert werden, deuten auf organisatorische Blockaden oder unzureichende Priorisierung hin.`,
'Pruefen Sie, ob die Massnahme weiterhin erforderlich ist, und erstellen Sie einen konkreten Umsetzungsplan mit Verantwortlichkeiten und Fristen.'
)
}
// =============================================================================
// AGGREGATE CHECKS (4-10)
// =============================================================================
/**
* Check 4: INCOMPLETE_CATEGORY (HIGH)
* Category where ALL applicable (REQUIRED) controls are NOT_IMPLEMENTED.
*/
function checkIncompleteCategory(toms: DerivedTOM[]): TOMComplianceIssue[] {
const issues: TOMComplianceIssue[] = []
// Group applicable TOMs by category
const categoryMap = new Map<ControlCategory, DerivedTOM[]>()
for (const tom of toms) {
const control = getControlById(tom.controlId)
if (!control) continue
const category = control.category
if (!categoryMap.has(category)) {
categoryMap.set(category, [])
}
categoryMap.get(category)!.push(tom)
}
for (const [category, categoryToms] of Array.from(categoryMap.entries())) {
// Only check categories that have at least one REQUIRED control
const requiredToms = categoryToms.filter((t: DerivedTOM) => t.applicability === 'REQUIRED')
if (requiredToms.length === 0) continue
const allNotImplemented = requiredToms.every((t: DerivedTOM) => t.implementationStatus === 'NOT_IMPLEMENTED')
if (allNotImplemented) {
issues.push(
createIssue(
category,
category,
'INCOMPLETE_CATEGORY',
'HIGH',
`Kategorie "${category}" vollstaendig ohne Umsetzung`,
`Alle ${requiredToms.length} Pflichtmassnahme(n) in der Kategorie "${category}" sind nicht umgesetzt. Eine vollstaendig unabgedeckte Kategorie stellt eine erhebliche Luecke im TOM-Konzept dar.`,
`Setzen Sie mindestens die wichtigsten Massnahmen in der Kategorie "${category}" um, um eine Grundabdeckung sicherzustellen.`
)
)
}
}
return issues
}
/**
* Check 5: NO_ENCRYPTION_MEASURES (CRITICAL)
* No ENCRYPTION control with status IMPLEMENTED.
*/
function checkNoEncryption(toms: DerivedTOM[]): TOMComplianceIssue | null {
const hasImplementedEncryption = toms.some((tom) => {
const control = getControlById(tom.controlId)
return control?.category === 'ENCRYPTION' && tom.implementationStatus === 'IMPLEMENTED'
})
if (!hasImplementedEncryption) {
return createIssue(
'ENCRYPTION',
'Verschluesselung',
'NO_ENCRYPTION_MEASURES',
'CRITICAL',
'Keine Verschluesselungsmassnahmen umgesetzt',
'Es ist keine einzige Verschluesselungsmassnahme als IMPLEMENTED markiert. Art. 32 Abs. 1 lit. a DSGVO nennt Verschluesselung explizit als geeignete technische Massnahme. Ohne Verschluesselung sind personenbezogene Daten bei Zugriff oder Verlust ungeschuetzt.',
'Implementieren Sie umgehend Verschluesselungsmassnahmen fuer Daten im Ruhezustand (Encryption at Rest) und waehrend der Uebertragung (Encryption in Transit).'
)
}
return null
}
/**
* Check 6: NO_PSEUDONYMIZATION (MEDIUM)
* DataProfile has special categories (Art. 9) but no PSEUDONYMIZATION control implemented.
*/
function checkNoPseudonymization(toms: DerivedTOM[], dataProfile: DataProfile | null): TOMComplianceIssue | null {
if (!dataProfile || !dataProfile.hasSpecialCategories) return null
const hasImplementedPseudonymization = toms.some((tom) => {
const control = getControlById(tom.controlId)
return control?.category === 'PSEUDONYMIZATION' && tom.implementationStatus === 'IMPLEMENTED'
})
if (!hasImplementedPseudonymization) {
return createIssue(
'PSEUDONYMIZATION',
'Pseudonymisierung',
'NO_PSEUDONYMIZATION',
'MEDIUM',
'Keine Pseudonymisierung bei besonderen Datenkategorien',
'Das Datenprofil enthaelt besondere Kategorien personenbezogener Daten (Art. 9 DSGVO), aber keine Pseudonymisierungsmassnahme ist umgesetzt. Art. 32 Abs. 1 lit. a DSGVO empfiehlt Pseudonymisierung ausdruecklich als Schutzmassnahme.',
'Implementieren Sie Pseudonymisierungsmassnahmen fuer die Verarbeitung besonderer Datenkategorien, um das Risiko fuer betroffene Personen zu minimieren.'
)
}
return null
}
/**
* Check 7: MISSING_AVAILABILITY (HIGH)
* No AVAILABILITY or RECOVERY control implemented AND no DR plan in securityProfile.
*/
function checkMissingAvailability(toms: DerivedTOM[], state: TOMGeneratorState): TOMComplianceIssue | null {
const hasAvailabilityOrRecovery = toms.some((tom) => {
const control = getControlById(tom.controlId)
return (
(control?.category === 'AVAILABILITY' || control?.category === 'RECOVERY') &&
tom.implementationStatus === 'IMPLEMENTED'
)
})
const hasDRPlan = state.securityProfile?.hasDRPlan ?? false
if (!hasAvailabilityOrRecovery && !hasDRPlan) {
return createIssue(
'AVAILABILITY',
'Verfuegbarkeit / Wiederherstellbarkeit',
'MISSING_AVAILABILITY',
'HIGH',
'Keine Verfuegbarkeits- oder Wiederherstellungsmassnahmen',
'Weder Verfuegbarkeits- noch Wiederherstellungsmassnahmen sind umgesetzt, und es existiert kein Disaster-Recovery-Plan im Security-Profil. Art. 32 Abs. 1 lit. b und c DSGVO verlangen die Faehigkeit zur raschen Wiederherstellung der Verfuegbarkeit personenbezogener Daten.',
'Implementieren Sie Backup-Konzepte, Redundanzloesungen und einen Disaster-Recovery-Plan, um die Verfuegbarkeit und Wiederherstellbarkeit sicherzustellen.'
)
}
return null
}
/**
* Check 8: NO_REVIEW_PROCESS (MEDIUM)
* No REVIEW control implemented (Art. 32 Abs. 1 lit. d requires periodic review).
*/
function checkNoReviewProcess(toms: DerivedTOM[]): TOMComplianceIssue | null {
const hasImplementedReview = toms.some((tom) => {
const control = getControlById(tom.controlId)
return control?.category === 'REVIEW' && tom.implementationStatus === 'IMPLEMENTED'
})
if (!hasImplementedReview) {
return createIssue(
'REVIEW',
'Ueberpruefung & Bewertung',
'NO_REVIEW_PROCESS',
'MEDIUM',
'Kein Verfahren zur regelmaessigen Ueberpruefung',
'Es ist keine Ueberpruefungsmassnahme als IMPLEMENTED markiert. Art. 32 Abs. 1 lit. d DSGVO verlangt ein Verfahren zur regelmaessigen Ueberpruefung, Bewertung und Evaluierung der Wirksamkeit der technischen und organisatorischen Massnahmen.',
'Implementieren Sie einen regelmaessigen Review-Prozess (z.B. quartalsweise TOM-Audits, jaehrliche Wirksamkeitspruefung) und dokumentieren Sie die Ergebnisse.'
)
}
return null
}
/**
* Check 9: UNCOVERED_SDM_GOAL (HIGH)
* SDM goal with 0% coverage — no implemented control maps to it via SDM_CATEGORY_MAPPING.
*/
function checkUncoveredSDMGoal(toms: DerivedTOM[]): TOMComplianceIssue[] {
const issues: TOMComplianceIssue[] = []
// Build reverse mapping: SDM goal -> ControlCategories that cover it
const sdmGoals = [
'Verfuegbarkeit',
'Integritaet',
'Vertraulichkeit',
'Nichtverkettung',
'Intervenierbarkeit',
'Transparenz',
'Datenminimierung',
] as const
const goalToCategoriesMap = new Map<string, ControlCategory[]>()
for (const goal of sdmGoals) {
goalToCategoriesMap.set(goal, [])
}
// Build reverse lookup from SDM_CATEGORY_MAPPING
for (const [category, goals] of Object.entries(SDM_CATEGORY_MAPPING)) {
for (const goal of goals) {
const existing = goalToCategoriesMap.get(goal)
if (existing) {
existing.push(category as ControlCategory)
}
}
}
// Collect implemented categories
const implementedCategories = new Set<ControlCategory>()
for (const tom of toms) {
if (tom.implementationStatus !== 'IMPLEMENTED') continue
const control = getControlById(tom.controlId)
if (control) {
implementedCategories.add(control.category)
}
}
// Check each SDM goal
for (const goal of sdmGoals) {
const coveringCategories = goalToCategoriesMap.get(goal) ?? []
const hasCoverage = coveringCategories.some((cat) => implementedCategories.has(cat))
if (!hasCoverage) {
issues.push(
createIssue(
`SDM-${goal}`,
goal,
'UNCOVERED_SDM_GOAL',
'HIGH',
`SDM-Gewaehrleistungsziel "${goal}" nicht abgedeckt`,
`Das Gewaehrleistungsziel "${goal}" des Standard-Datenschutzmodells (SDM) ist durch keine umgesetzte Massnahme abgedeckt. Zugehoerige Kategorien (${coveringCategories.join(', ')}) haben keine IMPLEMENTED Controls. Das SDM ist die anerkannte Methodik zur Umsetzung der DSGVO-Anforderungen.`,
`Setzen Sie mindestens eine Massnahme aus den Kategorien ${coveringCategories.join(', ')} um, um das SDM-Ziel "${goal}" abzudecken.`
)
)
}
}
return issues
}
/**
* Check 10: HIGH_RISK_WITHOUT_MEASURES (CRITICAL)
* Protection level VERY_HIGH but < 50% of REQUIRED controls implemented.
*/
function checkHighRiskWithoutMeasures(toms: DerivedTOM[], riskProfile: RiskProfile | null): TOMComplianceIssue | null {
if (!riskProfile || riskProfile.protectionLevel !== 'VERY_HIGH') return null
const requiredToms = toms.filter((t) => t.applicability === 'REQUIRED')
if (requiredToms.length === 0) return null
const implementedCount = requiredToms.filter((t) => t.implementationStatus === 'IMPLEMENTED').length
const implementationRate = implementedCount / requiredToms.length
if (implementationRate < 0.5) {
const percentage = Math.round(implementationRate * 100)
return createIssue(
'RISK-PROFILE',
'Risikoprofil VERY_HIGH',
'HIGH_RISK_WITHOUT_MEASURES',
'CRITICAL',
'Sehr hoher Schutzbedarf bei niedriger Umsetzungsrate',
`Der Schutzbedarf ist als VERY_HIGH eingestuft, aber nur ${implementedCount} von ${requiredToms.length} Pflichtmassnahmen (${percentage}%) sind umgesetzt. Bei sehr hohem Schutzbedarf muessen mindestens 50% der Pflichtmassnahmen implementiert sein, um ein angemessenes Schutzniveau gemaess Art. 32 DSGVO zu gewaehrleisten.`,
'Priorisieren Sie die Umsetzung der verbleibenden Pflichtmassnahmen. Beginnen Sie mit CRITICAL- und HIGH-Priority Controls. Erwaeegen Sie einen Umsetzungsplan mit klaren Meilensteinen.'
)
}
return null
}
// =============================================================================
// MAIN COMPLIANCE CHECK
// =============================================================================
/**
* Fuehrt einen vollstaendigen Compliance-Check ueber alle TOMs durch.
*
* @param state - Der vollstaendige TOMGeneratorState
* @returns TOMComplianceCheckResult mit Issues, Score und Statistiken
*/
export function runTOMComplianceCheck(state: TOMGeneratorState): TOMComplianceCheckResult {
// Reset counter for deterministic IDs within a single check run
issueCounter = 0
const issues: TOMComplianceIssue[] = []
// Filter to applicable TOMs only (REQUIRED or RECOMMENDED, exclude NOT_APPLICABLE)
const applicableTOMs = state.derivedTOMs.filter(
(tom) => tom.applicability === 'REQUIRED' || tom.applicability === 'RECOMMENDED'
)
// Run per-TOM checks (1-3, 11) on each applicable TOM
for (const tom of applicableTOMs) {
const perTomChecks = [
checkMissingResponsible(tom),
checkOverdueReview(tom),
checkMissingEvidence(tom),
checkStaleNotImplemented(tom, state),
]
for (const issue of perTomChecks) {
if (issue !== null) {
issues.push(issue)
}
}
}
// Run aggregate checks (4-10)
issues.push(...checkIncompleteCategory(applicableTOMs))
const aggregateChecks = [
checkNoEncryption(applicableTOMs),
checkNoPseudonymization(applicableTOMs, state.dataProfile),
checkMissingAvailability(applicableTOMs, state),
checkNoReviewProcess(applicableTOMs),
checkHighRiskWithoutMeasures(applicableTOMs, state.riskProfile),
]
for (const issue of aggregateChecks) {
if (issue !== null) {
issues.push(issue)
}
}
issues.push(...checkUncoveredSDMGoal(applicableTOMs))
// Calculate score
const bySeverity: Record<TOMComplianceIssueSeverity, number> = {
LOW: 0,
MEDIUM: 0,
HIGH: 0,
CRITICAL: 0,
}
for (const issue of issues) {
bySeverity[issue.severity]++
}
const rawScore =
100 -
(bySeverity.CRITICAL * 15 +
bySeverity.HIGH * 10 +
bySeverity.MEDIUM * 5 +
bySeverity.LOW * 2)
const score = Math.max(0, rawScore)
// Calculate pass/fail per TOM
const failedControlIds = new Set(
issues.filter((i) => !i.controlId.startsWith('SDM-') && i.controlId !== 'RISK-PROFILE').map((i) => i.controlId)
)
const totalTOMs = applicableTOMs.length
const failedCount = failedControlIds.size
const passedCount = Math.max(0, totalTOMs - failedCount)
return {
issues,
score,
stats: {
total: totalTOMs,
passed: passedCount,
failed: failedCount,
bySeverity,
},
}
}

View File

@@ -0,0 +1,906 @@
// =============================================================================
// TOM Module - TOM-Dokumentation Document Generator
// Generates a printable, audit-ready HTML document according to DSGVO Art. 32
// =============================================================================
import type {
TOMGeneratorState,
DerivedTOM,
CompanyProfile,
RiskProfile,
ControlCategory,
} from './tom-generator/types'
import { SDM_CATEGORY_MAPPING } from './tom-generator/types'
import {
getControlById,
getControlsByCategory,
getAllCategories,
getCategoryMetadata,
} from './tom-generator/controls/loader'
import type { TOMComplianceCheckResult, TOMComplianceIssueSeverity } from './tom-compliance'
// =============================================================================
// TYPES
// =============================================================================
export interface TOMDocumentOrgHeader {
organizationName: string
industry: string
dpoName: string
dpoContact: string
responsiblePerson: string
itSecurityContact: string
locations: string[]
employeeCount: string
documentVersion: string
lastReviewDate: string
nextReviewDate: string
reviewInterval: string
}
export interface TOMDocumentRevision {
version: string
date: string
author: string
changes: string
}
// =============================================================================
// DEFAULTS
// =============================================================================
export function createDefaultTOMDocumentOrgHeader(): TOMDocumentOrgHeader {
const now = new Date()
const nextYear = new Date()
nextYear.setFullYear(nextYear.getFullYear() + 1)
return {
organizationName: '',
industry: '',
dpoName: '',
dpoContact: '',
responsiblePerson: '',
itSecurityContact: '',
locations: [],
employeeCount: '',
documentVersion: '1.0',
lastReviewDate: now.toISOString().split('T')[0],
nextReviewDate: nextYear.toISOString().split('T')[0],
reviewInterval: 'Jaehrlich',
}
}
// =============================================================================
// SEVERITY LABELS (for Compliance Status section)
// =============================================================================
const SEVERITY_LABELS_DE: Record<TOMComplianceIssueSeverity, string> = {
CRITICAL: 'Kritisch',
HIGH: 'Hoch',
MEDIUM: 'Mittel',
LOW: 'Niedrig',
}
const SEVERITY_COLORS: Record<TOMComplianceIssueSeverity, string> = {
CRITICAL: '#dc2626',
HIGH: '#ea580c',
MEDIUM: '#d97706',
LOW: '#6b7280',
}
// =============================================================================
// CATEGORY LABELS (German)
// =============================================================================
const CATEGORY_LABELS_DE: Record<ControlCategory, string> = {
ACCESS_CONTROL: 'Zutrittskontrolle',
ADMISSION_CONTROL: 'Zugangskontrolle',
ACCESS_AUTHORIZATION: 'Zugriffskontrolle',
TRANSFER_CONTROL: 'Weitergabekontrolle',
INPUT_CONTROL: 'Eingabekontrolle',
ORDER_CONTROL: 'Auftragskontrolle',
AVAILABILITY: 'Verfuegbarkeit',
SEPARATION: 'Trennbarkeit',
ENCRYPTION: 'Verschluesselung',
PSEUDONYMIZATION: 'Pseudonymisierung',
RESILIENCE: 'Belastbarkeit',
RECOVERY: 'Wiederherstellbarkeit',
REVIEW: 'Ueberpruefung & Bewertung',
}
// =============================================================================
// STATUS & APPLICABILITY LABELS
// =============================================================================
const STATUS_LABELS_DE: Record<string, string> = {
IMPLEMENTED: 'Umgesetzt',
PARTIAL: 'Teilweise umgesetzt',
NOT_IMPLEMENTED: 'Nicht umgesetzt',
}
const STATUS_BADGE_CLASSES: Record<string, string> = {
IMPLEMENTED: 'badge-active',
PARTIAL: 'badge-review',
NOT_IMPLEMENTED: 'badge-critical',
}
const APPLICABILITY_LABELS_DE: Record<string, string> = {
REQUIRED: 'Erforderlich',
RECOMMENDED: 'Empfohlen',
OPTIONAL: 'Optional',
NOT_APPLICABLE: 'Nicht anwendbar',
}
// =============================================================================
// HTML DOCUMENT BUILDER
// =============================================================================
export function buildTOMDocumentHtml(
derivedTOMs: DerivedTOM[],
orgHeader: TOMDocumentOrgHeader,
companyProfile: CompanyProfile | null,
riskProfile: RiskProfile | null,
complianceResult: TOMComplianceCheckResult | null,
revisions: TOMDocumentRevision[]
): string {
const today = new Date().toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})
const orgName = orgHeader.organizationName || 'Organisation'
// Filter out NOT_APPLICABLE TOMs for display
const applicableTOMs = derivedTOMs.filter(t => t.applicability !== 'NOT_APPLICABLE')
// Group TOMs by category via control library lookup
const tomsByCategory = new Map<ControlCategory, DerivedTOM[]>()
for (const tom of applicableTOMs) {
const control = getControlById(tom.controlId)
const cat = control?.category || 'REVIEW'
if (!tomsByCategory.has(cat)) tomsByCategory.set(cat, [])
tomsByCategory.get(cat)!.push(tom)
}
// Build role map: role/department → list of control codes
const roleMap = new Map<string, string[]>()
for (const tom of applicableTOMs) {
const role = tom.responsiblePerson || tom.responsibleDepartment || 'Nicht zugewiesen'
if (!roleMap.has(role)) roleMap.set(role, [])
const control = getControlById(tom.controlId)
roleMap.get(role)!.push(control?.code || tom.controlId)
}
// =========================================================================
// HTML Template
// =========================================================================
let html = `<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>TOM-Dokumentation — ${escHtml(orgName)}</title>
<style>
@page { size: A4; margin: 20mm 18mm 22mm 18mm; }
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: 10pt;
line-height: 1.5;
color: #1e293b;
}
/* Cover */
.cover {
min-height: 90vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
page-break-after: always;
}
.cover h1 {
font-size: 28pt;
color: #5b21b6;
margin-bottom: 8px;
font-weight: 700;
}
.cover .subtitle {
font-size: 14pt;
color: #7c3aed;
margin-bottom: 40px;
}
.cover .org-info {
background: #f5f3ff;
border: 1px solid #ddd6fe;
border-radius: 8px;
padding: 24px 40px;
text-align: left;
width: 400px;
margin-bottom: 24px;
}
.cover .org-info div { margin-bottom: 6px; }
.cover .org-info .label { font-weight: 600; color: #5b21b6; display: inline-block; min-width: 160px; }
.cover .legal-ref {
font-size: 9pt;
color: #64748b;
margin-top: 20px;
}
/* TOC */
.toc {
page-break-after: always;
padding-top: 40px;
}
.toc h2 {
font-size: 18pt;
color: #5b21b6;
margin-bottom: 20px;
border-bottom: 2px solid #5b21b6;
padding-bottom: 8px;
}
.toc-entry {
display: flex;
justify-content: space-between;
padding: 6px 0;
border-bottom: 1px dotted #cbd5e1;
font-size: 10pt;
}
.toc-entry .toc-num { font-weight: 600; color: #5b21b6; min-width: 40px; }
/* Sections */
.section {
page-break-inside: avoid;
margin-bottom: 24px;
}
.section-header {
font-size: 14pt;
color: #5b21b6;
font-weight: 700;
margin: 30px 0 12px 0;
border-bottom: 2px solid #ddd6fe;
padding-bottom: 6px;
}
.section-body { margin-bottom: 16px; }
/* Tables */
table {
width: 100%;
border-collapse: collapse;
margin: 10px 0 16px 0;
font-size: 9pt;
}
th, td {
border: 1px solid #e2e8f0;
padding: 6px 8px;
text-align: left;
vertical-align: top;
}
th {
background: #f5f3ff;
color: #5b21b6;
font-weight: 600;
font-size: 8.5pt;
text-transform: uppercase;
letter-spacing: 0.3px;
}
tr:nth-child(even) td { background: #faf5ff; }
/* Detail cards */
.policy-detail {
page-break-inside: avoid;
border: 1px solid #e2e8f0;
border-radius: 6px;
margin-bottom: 16px;
overflow: hidden;
}
.policy-detail-header {
background: #f5f3ff;
padding: 8px 12px;
font-weight: 700;
color: #5b21b6;
border-bottom: 1px solid #ddd6fe;
display: flex;
justify-content: space-between;
}
.policy-detail-body { padding: 0; }
.policy-detail-body table { margin: 0; }
.policy-detail-body th { width: 200px; }
/* Badges */
.badge {
display: inline-block;
padding: 1px 8px;
border-radius: 9999px;
font-size: 8pt;
font-weight: 600;
}
.badge-active { background: #dcfce7; color: #166534; }
.badge-draft { background: #f3f4f6; color: #374151; }
.badge-review { background: #fef9c3; color: #854d0e; }
.badge-critical { background: #fecaca; color: #991b1b; }
.badge-high { background: #fed7aa; color: #9a3412; }
.badge-medium { background: #fef3c7; color: #92400e; }
.badge-low { background: #f3f4f6; color: #4b5563; }
/* Principles */
.principle {
margin-bottom: 10px;
padding-left: 20px;
position: relative;
}
.principle::before {
content: '';
position: absolute;
left: 0;
top: 6px;
width: 10px;
height: 10px;
background: #7c3aed;
border-radius: 50%;
}
.principle strong { color: #5b21b6; }
/* Score */
.score-box {
display: inline-block;
padding: 4px 16px;
border-radius: 8px;
font-size: 18pt;
font-weight: 700;
margin-right: 12px;
}
.score-excellent { background: #dcfce7; color: #166534; }
.score-good { background: #dbeafe; color: #1e40af; }
.score-needs-work { background: #fef3c7; color: #92400e; }
.score-poor { background: #fecaca; color: #991b1b; }
/* Footer */
.page-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 8px 18mm;
font-size: 7.5pt;
color: #94a3b8;
display: flex;
justify-content: space-between;
border-top: 1px solid #e2e8f0;
}
/* Print */
@media print {
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
.no-print { display: none !important; }
.page-break { page-break-after: always; }
}
</style>
</head>
<body>
`
// =========================================================================
// Section 0: Cover Page
// =========================================================================
html += `
<div class="cover">
<h1>TOM-Dokumentation</h1>
<div class="subtitle">Technische und Organisatorische Massnahmen gemaess Art. 32 DSGVO</div>
<div class="org-info">
<div><span class="label">Organisation:</span> ${escHtml(orgName)}</div>
${orgHeader.industry ? `<div><span class="label">Branche:</span> ${escHtml(orgHeader.industry)}</div>` : ''}
${orgHeader.dpoName ? `<div><span class="label">DSB:</span> ${escHtml(orgHeader.dpoName)}</div>` : ''}
${orgHeader.dpoContact ? `<div><span class="label">DSB-Kontakt:</span> ${escHtml(orgHeader.dpoContact)}</div>` : ''}
${orgHeader.responsiblePerson ? `<div><span class="label">Verantwortlicher:</span> ${escHtml(orgHeader.responsiblePerson)}</div>` : ''}
${orgHeader.itSecurityContact ? `<div><span class="label">IT-Sicherheit:</span> ${escHtml(orgHeader.itSecurityContact)}</div>` : ''}
${orgHeader.employeeCount ? `<div><span class="label">Mitarbeiter:</span> ${escHtml(orgHeader.employeeCount)}</div>` : ''}
${orgHeader.locations.length > 0 ? `<div><span class="label">Standorte:</span> ${escHtml(orgHeader.locations.join(', '))}</div>` : ''}
</div>
<div class="legal-ref">
Version ${escHtml(orgHeader.documentVersion)} | Stand: ${today}<br/>
Letzte Pruefung: ${formatDateDE(orgHeader.lastReviewDate)} | Naechste Pruefung: ${formatDateDE(orgHeader.nextReviewDate)}<br/>
Pruefintervall: ${escHtml(orgHeader.reviewInterval)}
</div>
</div>
`
// =========================================================================
// Table of Contents
// =========================================================================
const sections = [
'Ziel und Zweck',
'Geltungsbereich',
'Grundprinzipien Art. 32',
'Schutzbedarf und Risikoanalyse',
'Massnahmen-Uebersicht',
'Detaillierte Massnahmen',
'SDM Gewaehrleistungsziele',
'Verantwortlichkeiten',
'Pruef- und Revisionszyklus',
'Compliance-Status',
'Aenderungshistorie',
]
html += `
<div class="toc">
<h2>Inhaltsverzeichnis</h2>
${sections.map((s, i) => `<div class="toc-entry"><span><span class="toc-num">${i + 1}.</span> ${escHtml(s)}</span></div>`).join('\n ')}
</div>
`
// =========================================================================
// Section 1: Ziel und Zweck
// =========================================================================
html += `
<div class="section">
<div class="section-header">1. Ziel und Zweck</div>
<div class="section-body">
<p>Diese TOM-Dokumentation beschreibt die technischen und organisatorischen Massnahmen
zum Schutz personenbezogener Daten bei <strong>${escHtml(orgName)}</strong>. Sie dient
der Umsetzung folgender DSGVO-Anforderungen:</p>
<table>
<tr><th>Rechtsgrundlage</th><th>Inhalt</th></tr>
<tr><td><strong>Art. 32 Abs. 1 lit. a DSGVO</strong></td><td>Pseudonymisierung und Verschluesselung personenbezogener Daten</td></tr>
<tr><td><strong>Art. 32 Abs. 1 lit. b DSGVO</strong></td><td>Faehigkeit, die Vertraulichkeit, Integritaet, Verfuegbarkeit und Belastbarkeit der Systeme und Dienste im Zusammenhang mit der Verarbeitung auf Dauer sicherzustellen</td></tr>
<tr><td><strong>Art. 32 Abs. 1 lit. c DSGVO</strong></td><td>Faehigkeit, die Verfuegbarkeit der personenbezogenen Daten und den Zugang zu ihnen bei einem physischen oder technischen Zwischenfall rasch wiederherzustellen</td></tr>
<tr><td><strong>Art. 32 Abs. 1 lit. d DSGVO</strong></td><td>Verfahren zur regelmaessigen Ueberpruefung, Bewertung und Evaluierung der Wirksamkeit der technischen und organisatorischen Massnahmen</td></tr>
</table>
<p>Die TOM-Dokumentation ist fester Bestandteil des Datenschutz-Managementsystems und wird
regelmaessig ueberprueft und aktualisiert.</p>
</div>
</div>
`
// =========================================================================
// Section 2: Geltungsbereich
// =========================================================================
const industryInfo = companyProfile?.industry || orgHeader.industry || ''
const hostingInfo = companyProfile ? `Unternehmen: ${escHtml(companyProfile.name || orgName)}, Groesse: ${escHtml(companyProfile.size || '-')}` : ''
html += `
<div class="section">
<div class="section-header">2. Geltungsbereich</div>
<div class="section-body">
<p>Diese TOM-Dokumentation gilt fuer alle IT-Systeme, Anwendungen und Verarbeitungsprozesse
von <strong>${escHtml(orgName)}</strong>${industryInfo ? ` (Branche: ${escHtml(industryInfo)})` : ''}.</p>
${hostingInfo ? `<p>${hostingInfo}</p>` : ''}
${orgHeader.locations.length > 0 ? `<p>Standorte: ${escHtml(orgHeader.locations.join(', '))}</p>` : ''}
<p>Die dokumentierten Massnahmen stammen aus zwei Quellen:</p>
<ul style="margin: 8px 0 8px 24px;">
<li><strong>Embedded Library (TOM-xxx):</strong> Integrierte Kontrollbibliothek mit spezifischen Massnahmen fuer Art. 32 DSGVO</li>
<li><strong>Canonical Control Library (CP-CLIB):</strong> Uebergreifende Kontrollbibliothek mit framework-uebergreifenden Massnahmen</li>
</ul>
<p>Insgesamt umfasst dieses Dokument <strong>${applicableTOMs.length}</strong> anwendbare Massnahmen
in <strong>${tomsByCategory.size}</strong> Kategorien.</p>
</div>
</div>
`
// =========================================================================
// Section 3: Grundprinzipien Art. 32
// =========================================================================
html += `
<div class="section">
<div class="section-header">3. Grundprinzipien Art. 32</div>
<div class="section-body">
<div class="principle"><strong>Vertraulichkeit:</strong> Schutz personenbezogener Daten vor unbefugter Kenntnisnahme durch Zutrittskontrolle, Zugangskontrolle, Zugriffskontrolle und Verschluesselung (Art. 32 Abs. 1 lit. b DSGVO).</div>
<div class="principle"><strong>Integritaet:</strong> Sicherstellung, dass personenbezogene Daten nicht unbefugt oder unbeabsichtigt veraendert werden koennen, durch Eingabekontrolle, Weitergabekontrolle und Protokollierung (Art. 32 Abs. 1 lit. b DSGVO).</div>
<div class="principle"><strong>Verfuegbarkeit und Belastbarkeit:</strong> Gewaehrleistung, dass Systeme und Dienste bei Lastspitzen und Stoerungen zuverlaessig funktionieren, durch Backup, Redundanz und Disaster Recovery (Art. 32 Abs. 1 lit. b DSGVO).</div>
<div class="principle"><strong>Rasche Wiederherstellbarkeit:</strong> Faehigkeit, nach einem physischen oder technischen Zwischenfall Daten und Systeme schnell wiederherzustellen, durch getestete Recovery-Prozesse (Art. 32 Abs. 1 lit. c DSGVO).</div>
<div class="principle"><strong>Regelmaessige Wirksamkeitspruefung:</strong> Verfahren zur regelmaessigen Ueberpruefung, Bewertung und Evaluierung der Wirksamkeit aller technischen und organisatorischen Massnahmen (Art. 32 Abs. 1 lit. d DSGVO).</div>
</div>
</div>
`
// =========================================================================
// Section 4: Schutzbedarf und Risikoanalyse
// =========================================================================
html += `
<div class="section">
<div class="section-header">4. Schutzbedarf und Risikoanalyse</div>
<div class="section-body">
`
if (riskProfile) {
html += ` <p>Die folgende Schutzbedarfsanalyse bildet die Grundlage fuer die Auswahl und Priorisierung
der technischen und organisatorischen Massnahmen:</p>
<table>
<tr><th>Kriterium</th><th>Bewertung</th></tr>
<tr><td>Vertraulichkeit</td><td>${riskProfile.ciaAssessment.confidentiality}/5</td></tr>
<tr><td>Integritaet</td><td>${riskProfile.ciaAssessment.integrity}/5</td></tr>
<tr><td>Verfuegbarkeit</td><td>${riskProfile.ciaAssessment.availability}/5</td></tr>
<tr><td>Schutzniveau</td><td><strong>${escHtml(riskProfile.protectionLevel)}</strong></td></tr>
<tr><td>DSFA-Pflicht</td><td>${riskProfile.dsfaRequired ? 'Ja' : 'Nein'}</td></tr>
${riskProfile.specialRisks.length > 0 ? `<tr><td>Spezialrisiken</td><td>${escHtml(riskProfile.specialRisks.join(', '))}</td></tr>` : ''}
${riskProfile.regulatoryRequirements.length > 0 ? `<tr><td>Regulatorische Anforderungen</td><td>${escHtml(riskProfile.regulatoryRequirements.join(', '))}</td></tr>` : ''}
</table>
`
} else {
html += ` <p><em>Die Schutzbedarfsanalyse wurde noch nicht durchgefuehrt. Fuehren Sie den
Risiko-Wizard im TOM-Generator durch, um den Schutzbedarf zu ermitteln.</em></p>
`
}
html += ` </div>
</div>
`
// =========================================================================
// Section 5: Massnahmen-Uebersicht
// =========================================================================
html += `
<div class="section page-break">
<div class="section-header">5. Massnahmen-Uebersicht</div>
<div class="section-body">
<p>Die folgende Tabelle zeigt eine Uebersicht aller ${applicableTOMs.length} anwendbaren Massnahmen
nach Kategorie:</p>
<table>
<tr>
<th>Kategorie</th>
<th>Gesamt</th>
<th>Umgesetzt</th>
<th>Teilweise</th>
<th>Offen</th>
</tr>
`
const allCategories = getAllCategories()
for (const cat of allCategories) {
const tomsInCat = tomsByCategory.get(cat)
if (!tomsInCat || tomsInCat.length === 0) continue
const implemented = tomsInCat.filter(t => t.implementationStatus === 'IMPLEMENTED').length
const partial = tomsInCat.filter(t => t.implementationStatus === 'PARTIAL').length
const notImpl = tomsInCat.filter(t => t.implementationStatus === 'NOT_IMPLEMENTED').length
const catLabel = CATEGORY_LABELS_DE[cat] || cat
html += ` <tr>
<td>${escHtml(catLabel)}</td>
<td>${tomsInCat.length}</td>
<td>${implemented}</td>
<td>${partial}</td>
<td>${notImpl}</td>
</tr>
`
}
html += ` </table>
</div>
</div>
`
// =========================================================================
// Section 6: Detaillierte Massnahmen
// =========================================================================
html += `
<div class="section">
<div class="section-header">6. Detaillierte Massnahmen</div>
<div class="section-body">
`
for (const cat of allCategories) {
const tomsInCat = tomsByCategory.get(cat)
if (!tomsInCat || tomsInCat.length === 0) continue
const catLabel = CATEGORY_LABELS_DE[cat] || cat
const catMeta = getCategoryMetadata(cat)
const gdprRef = catMeta?.gdprReference || ''
html += ` <h3 style="color: #5b21b6; margin: 20px 0 10px 0; font-size: 11pt;">${escHtml(catLabel)}${gdprRef ? ` <span style="font-weight: 400; font-size: 9pt; color: #64748b;">(${escHtml(gdprRef)})</span>` : ''}</h3>
`
// Sort TOMs by control code
const sortedTOMs = [...tomsInCat].sort((a, b) => {
const codeA = getControlById(a.controlId)?.code || a.controlId
const codeB = getControlById(b.controlId)?.code || b.controlId
return codeA.localeCompare(codeB)
})
for (const tom of sortedTOMs) {
const control = getControlById(tom.controlId)
const code = control?.code || tom.controlId
const nameDE = control?.name?.de || tom.name
const descDE = control?.description?.de || tom.description
const typeLabel = control?.type === 'TECHNICAL' ? 'Technisch' : control?.type === 'ORGANIZATIONAL' ? 'Organisatorisch' : '-'
const statusLabel = STATUS_LABELS_DE[tom.implementationStatus] || tom.implementationStatus
const statusBadge = STATUS_BADGE_CLASSES[tom.implementationStatus] || 'badge-draft'
const applicabilityLabel = APPLICABILITY_LABELS_DE[tom.applicability] || tom.applicability
const responsible = [tom.responsiblePerson, tom.responsibleDepartment].filter(s => s && s.trim()).join(' / ') || '-'
const implDate = tom.implementationDate ? formatDateDE(typeof tom.implementationDate === 'string' ? tom.implementationDate : tom.implementationDate.toISOString()) : '-'
const reviewDate = tom.reviewDate ? formatDateDE(typeof tom.reviewDate === 'string' ? tom.reviewDate : tom.reviewDate.toISOString()) : '-'
// Evidence
const evidenceInfo = tom.linkedEvidence.length > 0
? tom.linkedEvidence.join(', ')
: tom.evidenceGaps.length > 0
? `<em style="color: #d97706;">Fehlend: ${escHtml(tom.evidenceGaps.join(', '))}</em>`
: '-'
// Framework mappings
let mappingsHtml = '-'
if (control?.mappings && control.mappings.length > 0) {
mappingsHtml = control.mappings.map(m => `${escHtml(m.framework)}: ${escHtml(m.reference)}`).join('<br/>')
}
html += `
<div class="policy-detail">
<div class="policy-detail-header">
<span>${escHtml(code)}${escHtml(nameDE)}</span>
<span class="badge ${statusBadge}">${escHtml(statusLabel)}</span>
</div>
<div class="policy-detail-body">
<table>
<tr><th>Beschreibung</th><td>${escHtml(descDE)}</td></tr>
<tr><th>Massnahmentyp</th><td>${escHtml(typeLabel)}</td></tr>
<tr><th>Anwendbarkeit</th><td>${escHtml(applicabilityLabel)}${tom.applicabilityReason ? `${escHtml(tom.applicabilityReason)}` : ''}</td></tr>
<tr><th>Umsetzungsstatus</th><td><span class="badge ${statusBadge}">${escHtml(statusLabel)}</span></td></tr>
<tr><th>Verantwortlich</th><td>${escHtml(responsible)}</td></tr>
<tr><th>Umsetzungsdatum</th><td>${implDate}</td></tr>
<tr><th>Naechste Pruefung</th><td>${reviewDate}</td></tr>
<tr><th>Evidence</th><td>${evidenceInfo}</td></tr>
<tr><th>Framework-Mappings</th><td>${mappingsHtml}</td></tr>
</table>
</div>
</div>
`
}
}
html += ` </div>
</div>
`
// =========================================================================
// Section 7: SDM Gewaehrleistungsziele
// =========================================================================
const sdmGoals: Array<{ goal: string; categories: ControlCategory[] }> = []
const allSDMGoals = [
'Verfuegbarkeit',
'Integritaet',
'Vertraulichkeit',
'Nichtverkettung',
'Intervenierbarkeit',
'Transparenz',
'Datenminimierung',
] as const
for (const goal of allSDMGoals) {
const cats: ControlCategory[] = []
for (const [cat, goals] of Object.entries(SDM_CATEGORY_MAPPING)) {
if (goals.includes(goal)) {
cats.push(cat as ControlCategory)
}
}
sdmGoals.push({ goal, categories: cats })
}
html += `
<div class="section page-break">
<div class="section-header">7. SDM Gewaehrleistungsziele</div>
<div class="section-body">
<p>Die folgende Tabelle zeigt die Abdeckung der sieben Gewaehrleistungsziele des
Standard-Datenschutzmodells (SDM) durch die implementierten Massnahmen:</p>
<table>
<tr>
<th>Gewaehrleistungsziel</th>
<th>Abgedeckt</th>
<th>Gesamt</th>
<th>Abdeckung (%)</th>
</tr>
`
for (const { goal, categories } of sdmGoals) {
let totalInGoal = 0
let implementedInGoal = 0
for (const cat of categories) {
const tomsInCat = tomsByCategory.get(cat) || []
totalInGoal += tomsInCat.length
implementedInGoal += tomsInCat.filter(t => t.implementationStatus === 'IMPLEMENTED').length
}
const percentage = totalInGoal > 0 ? Math.round((implementedInGoal / totalInGoal) * 100) : 0
html += ` <tr>
<td>${escHtml(goal)}</td>
<td>${implementedInGoal}</td>
<td>${totalInGoal}</td>
<td>${percentage}%</td>
</tr>
`
}
html += ` </table>
</div>
</div>
`
// =========================================================================
// Section 8: Verantwortlichkeiten
// =========================================================================
html += `
<div class="section">
<div class="section-header">8. Verantwortlichkeiten</div>
<div class="section-body">
<p>Die folgende Rollenmatrix zeigt, welche Personen oder Abteilungen fuer welche Massnahmen
die Umsetzungsverantwortung tragen:</p>
<table>
<tr><th>Rolle / Verantwortlich</th><th>Massnahmen</th><th>Anzahl</th></tr>
`
for (const [role, controls] of roleMap.entries()) {
html += ` <tr>
<td>${escHtml(role)}</td>
<td>${controls.map(c => escHtml(c)).join(', ')}</td>
<td>${controls.length}</td>
</tr>
`
}
html += ` </table>
</div>
</div>
`
// =========================================================================
// Section 9: Pruef- und Revisionszyklus
// =========================================================================
html += `
<div class="section">
<div class="section-header">9. Pruef- und Revisionszyklus</div>
<div class="section-body">
<table>
<tr><th>Eigenschaft</th><th>Wert</th></tr>
<tr><td>Aktuelles Pruefintervall</td><td>${escHtml(orgHeader.reviewInterval)}</td></tr>
<tr><td>Letzte Pruefung</td><td>${formatDateDE(orgHeader.lastReviewDate)}</td></tr>
<tr><td>Naechste Pruefung</td><td>${formatDateDE(orgHeader.nextReviewDate)}</td></tr>
<tr><td>Aktuelle Version</td><td>${escHtml(orgHeader.documentVersion)}</td></tr>
</table>
<p style="margin-top: 8px;">Bei jeder Pruefung wird die TOM-Dokumentation auf folgende Punkte ueberprueft:</p>
<ul style="margin: 8px 0 8px 24px;">
<li>Vollstaendigkeit aller Massnahmen (neue Systeme oder Verarbeitungen erfasst?)</li>
<li>Aktualitaet des Umsetzungsstatus (Aenderungen seit letzter Pruefung?)</li>
<li>Wirksamkeit der technischen Massnahmen (Penetration-Tests, Audit-Ergebnisse)</li>
<li>Angemessenheit der organisatorischen Massnahmen (Schulungen, Richtlinien aktuell?)</li>
<li>Abdeckung aller SDM-Gewaehrleistungsziele</li>
<li>Zuordnung von Verantwortlichkeiten zu allen Massnahmen</li>
</ul>
</div>
</div>
`
// =========================================================================
// Section 10: Compliance-Status
// =========================================================================
html += `
<div class="section page-break">
<div class="section-header">10. Compliance-Status</div>
<div class="section-body">
`
if (complianceResult) {
const scoreClass = complianceResult.score >= 90 ? 'score-excellent'
: complianceResult.score >= 75 ? 'score-good'
: complianceResult.score >= 50 ? 'score-needs-work'
: 'score-poor'
const scoreLabel = complianceResult.score >= 90 ? 'Ausgezeichnet'
: complianceResult.score >= 75 ? 'Gut'
: complianceResult.score >= 50 ? 'Verbesserungswuerdig'
: 'Mangelhaft'
html += ` <p><span class="score-box ${scoreClass}">${complianceResult.score}/100</span> ${escHtml(scoreLabel)}</p>
<table style="margin-top: 12px;">
<tr><th>Kennzahl</th><th>Wert</th></tr>
<tr><td>Gepruefte Massnahmen</td><td>${complianceResult.stats.total}</td></tr>
<tr><td>Bestanden</td><td>${complianceResult.stats.passed}</td></tr>
<tr><td>Beanstandungen</td><td>${complianceResult.stats.failed}</td></tr>
</table>
`
if (complianceResult.issues.length > 0) {
html += ` <p style="margin-top: 12px;"><strong>Befunde nach Schweregrad:</strong></p>
<table>
<tr><th>Schweregrad</th><th>Anzahl</th><th>Befunde</th></tr>
`
const severityOrder: TOMComplianceIssueSeverity[] = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']
for (const sev of severityOrder) {
const count = complianceResult.stats.bySeverity[sev]
if (count === 0) continue
const issuesForSev = complianceResult.issues.filter(i => i.severity === sev)
html += ` <tr>
<td><span class="badge badge-${sev.toLowerCase()}" style="color: ${SEVERITY_COLORS[sev]}">${SEVERITY_LABELS_DE[sev]}</span></td>
<td>${count}</td>
<td>${issuesForSev.map(i => escHtml(i.title)).join('; ')}</td>
</tr>
`
}
html += ` </table>
`
} else {
html += ` <p style="margin-top: 8px;"><em>Keine Beanstandungen. Alle Massnahmen sind konform.</em></p>
`
}
} else {
html += ` <p><em>Compliance-Check wurde noch nicht ausgefuehrt. Fuehren Sie den Check im
Export-Tab durch, um den Status in das Dokument aufzunehmen.</em></p>
`
}
html += ` </div>
</div>
`
// =========================================================================
// Section 11: Aenderungshistorie
// =========================================================================
html += `
<div class="section">
<div class="section-header">11. Aenderungshistorie</div>
<div class="section-body">
<table>
<tr><th>Version</th><th>Datum</th><th>Autor</th><th>Aenderungen</th></tr>
`
if (revisions.length > 0) {
for (const rev of revisions) {
html += ` <tr>
<td>${escHtml(rev.version)}</td>
<td>${formatDateDE(rev.date)}</td>
<td>${escHtml(rev.author)}</td>
<td>${escHtml(rev.changes)}</td>
</tr>
`
}
} else {
html += ` <tr>
<td>${escHtml(orgHeader.documentVersion)}</td>
<td>${today}</td>
<td>${escHtml(orgHeader.dpoName || orgHeader.responsiblePerson || '-')}</td>
<td>Erstversion der TOM-Dokumentation</td>
</tr>
`
}
html += ` </table>
</div>
</div>
`
// =========================================================================
// Footer
// =========================================================================
html += `
<div class="page-footer">
<span>TOM-Dokumentation — ${escHtml(orgName)}</span>
<span>Stand: ${today} | Version ${escHtml(orgHeader.documentVersion)}</span>
</div>
</body>
</html>`
return html
}
// =============================================================================
// INTERNAL HELPERS
// =============================================================================
function escHtml(str: string): string {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
}
function formatDateDE(dateStr: string | null | undefined): string {
if (!dateStr) return '-'
try {
const date = new Date(dateStr)
if (isNaN(date.getTime())) return '-'
return date.toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})
} catch {
return '-'
}
}

View File

@@ -89,9 +89,9 @@ export interface ControlLibrary {
const CONTROL_LIBRARY_DATA: ControlLibrary = {
metadata: {
version: '1.0.0',
lastUpdated: '2026-02-04',
totalControls: 60,
version: '1.1.0',
lastUpdated: '2026-03-19',
totalControls: 88,
},
categories: new Map([
[
@@ -2353,6 +2353,648 @@ const CONTROL_LIBRARY_DATA: ControlLibrary = {
complexity: 'MEDIUM',
tags: ['training', 'security-awareness', 'phishing', 'social-engineering'],
},
// =========================================================================
// NEW CONTROLS (v1.1.0) — 25 additional measures
// =========================================================================
// ENCRYPTION — 2 new
{
id: 'TOM-ENC-04',
code: 'TOM-ENC-04',
category: 'ENCRYPTION',
type: 'TECHNICAL',
name: { de: 'Zertifikatsmanagement (TLS/SSL)', en: 'Certificate Management (TLS/SSL)' },
description: {
de: 'Systematische Verwaltung, Ueberwachung und rechtzeitige Erneuerung aller TLS/SSL-Zertifikate zur Vermeidung von Sicherheitsluecken durch abgelaufene Zertifikate.',
en: 'Systematic management, monitoring and timely renewal of all TLS/SSL certificates to prevent security gaps from expired certificates.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. a' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.10.1.2' },
{ framework: 'BSI_IT_GRUNDSCHUTZ', reference: 'CON.1' },
],
applicabilityConditions: [
{ field: 'architectureProfile.encryptionInTransit', operator: 'EQUALS', value: true, result: 'REQUIRED', priority: 25 },
],
defaultApplicability: 'RECOMMENDED',
evidenceRequirements: ['Zertifikatsinventar', 'Monitoring-Konfiguration', 'Erneuerungsprotokolle'],
reviewFrequency: 'QUARTERLY',
priority: 'HIGH',
complexity: 'MEDIUM',
tags: ['encryption', 'certificates', 'tls'],
},
{
id: 'TOM-ENC-05',
code: 'TOM-ENC-05',
category: 'ENCRYPTION',
type: 'ORGANIZATIONAL',
name: { de: 'Schluesselmanagement-Policy', en: 'Key Management Policy' },
description: {
de: 'Dokumentierte Richtlinie fuer den gesamten Lebenszyklus kryptografischer Schluessel inkl. Erzeugung, Verteilung, Speicherung, Rotation und Vernichtung.',
en: 'Documented policy for the full lifecycle of cryptographic keys including generation, distribution, storage, rotation and destruction.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. a' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.10.1.2' },
],
applicabilityConditions: [
{ field: 'architectureProfile.encryptionAtRest', operator: 'EQUALS', value: true, result: 'REQUIRED', priority: 25 },
{ field: 'dataProfile.hasSpecialCategories', operator: 'EQUALS', value: true, result: 'REQUIRED', priority: 30 },
],
defaultApplicability: 'RECOMMENDED',
evidenceRequirements: ['Schluesselmanagement-Richtlinie', 'Schluesselrotationsplan'],
reviewFrequency: 'ANNUAL',
priority: 'HIGH',
complexity: 'LOW',
tags: ['encryption', 'key-management', 'policy'],
},
// PSEUDONYMIZATION — 2 new
{
id: 'TOM-PS-03',
code: 'TOM-PS-03',
category: 'PSEUDONYMIZATION',
type: 'TECHNICAL',
name: { de: 'Anonymisierung fuer Analysezwecke', en: 'Anonymization for Analytics' },
description: {
de: 'Technische Verfahren zur irreversiblen Anonymisierung personenbezogener Daten fuer statistische Auswertungen und Analysen.',
en: 'Technical procedures for irreversible anonymization of personal data for statistical evaluations and analyses.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. a' },
{ framework: 'GDPR_ART25', reference: 'Art. 25 Abs. 1' },
],
applicabilityConditions: [
{ field: 'dataProfile.dataVolume', operator: 'IN', value: ['HIGH', 'VERY_HIGH'], result: 'RECOMMENDED', priority: 15 },
],
defaultApplicability: 'OPTIONAL',
evidenceRequirements: ['Anonymisierungsverfahren-Dokumentation', 'Re-Identifizierungs-Risikoanalyse'],
reviewFrequency: 'ANNUAL',
priority: 'MEDIUM',
complexity: 'HIGH',
tags: ['pseudonymization', 'anonymization', 'analytics'],
},
{
id: 'TOM-PS-04',
code: 'TOM-PS-04',
category: 'PSEUDONYMIZATION',
type: 'ORGANIZATIONAL',
name: { de: 'Pseudonymisierungskonzept', en: 'Pseudonymization Concept' },
description: {
de: 'Dokumentiertes Konzept fuer die Pseudonymisierung personenbezogener Daten mit Definition der Verfahren, Zustaendigkeiten und Zuordnungsregeln.',
en: 'Documented concept for pseudonymization of personal data with definition of procedures, responsibilities and mapping rules.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. a' },
{ framework: 'GDPR_ART25', reference: 'Art. 25 Abs. 1' },
{ framework: 'BSI_IT_GRUNDSCHUTZ', reference: 'CON.2' },
],
applicabilityConditions: [
{ field: 'dataProfile.hasSpecialCategories', operator: 'EQUALS', value: true, result: 'REQUIRED', priority: 25 },
],
defaultApplicability: 'RECOMMENDED',
evidenceRequirements: ['Pseudonymisierungskonzept', 'Verfahrensdokumentation'],
reviewFrequency: 'ANNUAL',
priority: 'HIGH',
complexity: 'MEDIUM',
tags: ['pseudonymization', 'concept', 'documentation'],
},
// INPUT_CONTROL — 1 new
{
id: 'TOM-IN-05',
code: 'TOM-IN-05',
category: 'INPUT_CONTROL',
type: 'TECHNICAL',
name: { de: 'Automatisierte Eingabevalidierung', en: 'Automated Input Validation' },
description: {
de: 'Technische Validierung aller Benutzereingaben zur Verhinderung von Injection-Angriffen und Sicherstellung der Datenintegritaet.',
en: 'Technical validation of all user inputs to prevent injection attacks and ensure data integrity.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. b' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.14.2.5' },
],
applicabilityConditions: [],
defaultApplicability: 'REQUIRED',
evidenceRequirements: ['Validierungsregeln-Dokumentation', 'Penetrationstest-Berichte'],
reviewFrequency: 'QUARTERLY',
priority: 'HIGH',
complexity: 'MEDIUM',
tags: ['input-validation', 'security', 'injection-prevention'],
},
// ORDER_CONTROL — 2 new
{
id: 'TOM-OR-05',
code: 'TOM-OR-05',
category: 'ORDER_CONTROL',
type: 'ORGANIZATIONAL',
name: { de: 'Auftragsverarbeiter-Monitoring', en: 'Processor Monitoring' },
description: {
de: 'Regelmaessige Ueberpruefung und Bewertung der Datenschutz-Massnahmen bei Auftragsverarbeitern gemaess Art. 28 Abs. 3 lit. h DSGVO.',
en: 'Regular review and assessment of data protection measures at processors according to Art. 28(3)(h) GDPR.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 28 Abs. 3 lit. h' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.15.2.1' },
],
applicabilityConditions: [
{ field: 'architectureProfile.hasSubprocessors', operator: 'EQUALS', value: true, result: 'REQUIRED', priority: 25 },
],
defaultApplicability: 'RECOMMENDED',
evidenceRequirements: ['Audit-Berichte der Auftragsverarbeiter', 'Monitoring-Checklisten'],
reviewFrequency: 'ANNUAL',
priority: 'HIGH',
complexity: 'MEDIUM',
tags: ['order-control', 'processor', 'monitoring'],
},
{
id: 'TOM-OR-06',
code: 'TOM-OR-06',
category: 'ORDER_CONTROL',
type: 'ORGANIZATIONAL',
name: { de: 'Sub-Processor Management', en: 'Sub-Processor Management' },
description: {
de: 'Dokumentiertes Verfahren zur Genehmigung, Ueberwachung und Dokumentation von Unterauftragsverarbeitern.',
en: 'Documented process for approval, monitoring and documentation of sub-processors.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 28 Abs. 2, 4' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.15.1.2' },
],
applicabilityConditions: [
{ field: 'architectureProfile.hasSubprocessors', operator: 'EQUALS', value: true, result: 'REQUIRED', priority: 25 },
{ field: 'architectureProfile.subprocessorCount', operator: 'GREATER_THAN', value: 3, result: 'REQUIRED', priority: 20 },
],
defaultApplicability: 'RECOMMENDED',
evidenceRequirements: ['Sub-Processor-Register', 'Genehmigungsverfahren', 'Vertragsdokumentation'],
reviewFrequency: 'SEMI_ANNUAL',
priority: 'HIGH',
complexity: 'MEDIUM',
tags: ['order-control', 'sub-processor'],
},
// RESILIENCE — 2 new
{
id: 'TOM-RE-04',
code: 'TOM-RE-04',
category: 'RESILIENCE',
type: 'TECHNICAL',
name: { de: 'DDoS-Abwehr (erweitert)', en: 'DDoS Mitigation (Advanced)' },
description: {
de: 'Erweiterte DDoS-Schutzmassnahmen inkl. Traffic-Analyse, automatischer Mitigation und Incident-Response-Integration.',
en: 'Advanced DDoS protection measures including traffic analysis, automatic mitigation and incident response integration.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. b' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.13.1.1' },
],
applicabilityConditions: [
{ field: 'riskProfile.protectionLevel', operator: 'EQUALS', value: 'VERY_HIGH', result: 'REQUIRED', priority: 25 },
{ field: 'architectureProfile.hostingModel', operator: 'IN', value: ['PUBLIC_CLOUD', 'HYBRID'], result: 'RECOMMENDED', priority: 15 },
],
defaultApplicability: 'OPTIONAL',
evidenceRequirements: ['DDoS-Schutzkonzept (erweitert)', 'Mitigation-Berichte', 'Incident-Playbooks'],
reviewFrequency: 'QUARTERLY',
priority: 'HIGH',
complexity: 'HIGH',
tags: ['resilience', 'ddos', 'advanced'],
},
{
id: 'TOM-RE-05',
code: 'TOM-RE-05',
category: 'RESILIENCE',
type: 'ORGANIZATIONAL',
name: { de: 'Kapazitaetsplanung', en: 'Capacity Planning' },
description: {
de: 'Systematische Planung und Ueberwachung von IT-Kapazitaeten zur Sicherstellung der Systemverfuegbarkeit bei wachsender Nutzung.',
en: 'Systematic planning and monitoring of IT capacities to ensure system availability with growing usage.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. b' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.12.1.3' },
],
applicabilityConditions: [
{ field: 'dataProfile.dataVolume', operator: 'IN', value: ['HIGH', 'VERY_HIGH'], result: 'REQUIRED', priority: 20 },
],
defaultApplicability: 'RECOMMENDED',
evidenceRequirements: ['Kapazitaetsplan', 'Trend-Analysen', 'Skalierungskonzept'],
reviewFrequency: 'QUARTERLY',
priority: 'MEDIUM',
complexity: 'MEDIUM',
tags: ['resilience', 'capacity', 'planning'],
},
// RECOVERY — 2 new
{
id: 'TOM-RC-04',
code: 'TOM-RC-04',
category: 'RECOVERY',
type: 'TECHNICAL',
name: { de: 'Georedundantes Backup', en: 'Geo-Redundant Backup' },
description: {
de: 'Speicherung von Backup-Kopien an geografisch getrennten Standorten zum Schutz vor standortbezogenen Katastrophen.',
en: 'Storage of backup copies at geographically separated locations to protect against site-specific disasters.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. c' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.12.3.1' },
{ framework: 'BSI_IT_GRUNDSCHUTZ', reference: 'CON.3' },
],
applicabilityConditions: [
{ field: 'riskProfile.protectionLevel', operator: 'IN', value: ['HIGH', 'VERY_HIGH'], result: 'REQUIRED', priority: 25 },
{ field: 'riskProfile.ciaAssessment.availability', operator: 'GREATER_THAN', value: 3, result: 'REQUIRED', priority: 20 },
],
defaultApplicability: 'RECOMMENDED',
evidenceRequirements: ['Georedundanz-Konzept', 'Backup-Standort-Dokumentation', 'Wiederherstellungstests'],
reviewFrequency: 'SEMI_ANNUAL',
priority: 'HIGH',
complexity: 'HIGH',
tags: ['recovery', 'backup', 'geo-redundancy'],
},
{
id: 'TOM-RC-05',
code: 'TOM-RC-05',
category: 'RECOVERY',
type: 'ORGANIZATIONAL',
name: { de: 'Notfallwiederherstellungs-Tests', en: 'Disaster Recovery Testing' },
description: {
de: 'Regelmaessige Durchfuehrung und Dokumentation von Notfallwiederherstellungstests zur Validierung der RTO/RPO-Ziele.',
en: 'Regular execution and documentation of disaster recovery tests to validate RTO/RPO targets.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. c, d' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.17.1.3' },
],
applicabilityConditions: [
{ field: 'securityProfile.hasDRPlan', operator: 'EQUALS', value: true, result: 'REQUIRED', priority: 25 },
],
defaultApplicability: 'RECOMMENDED',
evidenceRequirements: ['DR-Testberichte', 'RTO/RPO-Messungen', 'Verbesserungsmassnahmen'],
reviewFrequency: 'SEMI_ANNUAL',
priority: 'HIGH',
complexity: 'MEDIUM',
tags: ['recovery', 'dr-testing', 'rto', 'rpo'],
},
// SEPARATION — 2 new
{
id: 'TOM-SE-05',
code: 'TOM-SE-05',
category: 'SEPARATION',
type: 'TECHNICAL',
name: { de: 'Netzwerksegmentierung', en: 'Network Segmentation' },
description: {
de: 'Aufteilung des Netzwerks in separate Sicherheitszonen mit kontrollierten Uebergaengen zur Begrenzung der Ausbreitung von Sicherheitsvorfaellen.',
en: 'Division of the network into separate security zones with controlled transitions to limit the spread of security incidents.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. b' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.13.1.3' },
{ framework: 'BSI_IT_GRUNDSCHUTZ', reference: 'NET.1.1' },
],
applicabilityConditions: [
{ field: 'architectureProfile.hostingModel', operator: 'IN', value: ['ON_PREMISE', 'PRIVATE_CLOUD', 'HYBRID'], result: 'REQUIRED', priority: 20 },
{ field: 'riskProfile.protectionLevel', operator: 'IN', value: ['HIGH', 'VERY_HIGH'], result: 'REQUIRED', priority: 25 },
],
defaultApplicability: 'RECOMMENDED',
evidenceRequirements: ['Netzwerkplan', 'Firewall-Regeln', 'Segmentierungskonzept'],
reviewFrequency: 'SEMI_ANNUAL',
priority: 'HIGH',
complexity: 'HIGH',
tags: ['separation', 'network', 'segmentation'],
},
{
id: 'TOM-SE-06',
code: 'TOM-SE-06',
category: 'SEPARATION',
type: 'TECHNICAL',
name: { de: 'Mandantenisolierung in Cloud', en: 'Tenant Isolation in Cloud' },
description: {
de: 'Technische Sicherstellung der vollstaendigen Datentrennung zwischen verschiedenen Mandanten in Multi-Tenant-Cloud-Umgebungen.',
en: 'Technical assurance of complete data separation between different tenants in multi-tenant cloud environments.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. b' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.13.1.3' },
],
applicabilityConditions: [
{ field: 'architectureProfile.multiTenancy', operator: 'EQUALS', value: 'MULTI_TENANT', result: 'REQUIRED', priority: 30 },
{ field: 'architectureProfile.hostingModel', operator: 'IN', value: ['PUBLIC_CLOUD', 'HYBRID'], result: 'RECOMMENDED', priority: 15 },
],
defaultApplicability: 'OPTIONAL',
evidenceRequirements: ['Mandantentrennungskonzept', 'Isolierungstests', 'Cloud-Security-Assessment'],
reviewFrequency: 'SEMI_ANNUAL',
priority: 'CRITICAL',
complexity: 'HIGH',
tags: ['separation', 'multi-tenant', 'cloud'],
},
// ACCESS_CONTROL — 1 new
{
id: 'TOM-AC-06',
code: 'TOM-AC-06',
category: 'ACCESS_CONTROL',
type: 'ORGANIZATIONAL',
name: { de: 'Besuchermanagement (erweitert)', en: 'Visitor Management (Extended)' },
description: {
de: 'Erweitertes Besuchermanagement mit Voranmeldung, Identitaetspruefung, Begleitpflicht und zeitlich begrenztem Zugang zu sicherheitsrelevanten Bereichen.',
en: 'Extended visitor management with pre-registration, identity verification, escort requirement and time-limited access to security-relevant areas.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. b' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.7.2' },
],
applicabilityConditions: [
{ field: 'riskProfile.protectionLevel', operator: 'IN', value: ['HIGH', 'VERY_HIGH'], result: 'REQUIRED', priority: 20 },
{ field: 'dataProfile.hasSpecialCategories', operator: 'EQUALS', value: true, result: 'RECOMMENDED', priority: 15 },
],
defaultApplicability: 'OPTIONAL',
evidenceRequirements: ['Besuchermanagement-Richtlinie', 'Besucherprotokolle', 'Zonenkonzept'],
reviewFrequency: 'ANNUAL',
priority: 'MEDIUM',
complexity: 'LOW',
tags: ['physical-security', 'visitors', 'extended'],
},
// ADMISSION_CONTROL — 1 new
{
id: 'TOM-ADM-06',
code: 'TOM-ADM-06',
category: 'ADMISSION_CONTROL',
type: 'TECHNICAL',
name: { de: 'Endpoint Detection & Response (EDR)', en: 'Endpoint Detection & Response (EDR)' },
description: {
de: 'Einsatz von EDR-Loesungen zur Erkennung und Abwehr von Bedrohungen auf Endgeraeten in Echtzeit.',
en: 'Deployment of EDR solutions for real-time threat detection and response on endpoints.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. b' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.12.2.1' },
{ framework: 'BSI_IT_GRUNDSCHUTZ', reference: 'OPS.1.1.4' },
],
applicabilityConditions: [
{ field: 'riskProfile.protectionLevel', operator: 'IN', value: ['HIGH', 'VERY_HIGH'], result: 'REQUIRED', priority: 25 },
{ field: 'companyProfile.size', operator: 'IN', value: ['LARGE', 'ENTERPRISE'], result: 'RECOMMENDED', priority: 10 },
],
defaultApplicability: 'RECOMMENDED',
evidenceRequirements: ['EDR-Konfiguration', 'Bedrohungsberichte', 'Incident-Response-Statistiken'],
reviewFrequency: 'QUARTERLY',
priority: 'HIGH',
complexity: 'HIGH',
tags: ['endpoint', 'edr', 'threat-detection'],
},
// ACCESS_AUTHORIZATION — 2 new
{
id: 'TOM-AZ-06',
code: 'TOM-AZ-06',
category: 'ACCESS_AUTHORIZATION',
type: 'TECHNICAL',
name: { de: 'API-Zugriffskontrolle', en: 'API Access Control' },
description: {
de: 'Implementierung von Authentifizierungs- und Autorisierungsmechanismen fuer APIs (OAuth 2.0, API-Keys, Rate Limiting).',
en: 'Implementation of authentication and authorization mechanisms for APIs (OAuth 2.0, API keys, rate limiting).',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. b' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.9.4.1' },
],
applicabilityConditions: [
{ field: 'architectureProfile.hostingModel', operator: 'IN', value: ['PUBLIC_CLOUD', 'HYBRID'], result: 'REQUIRED', priority: 20 },
],
defaultApplicability: 'RECOMMENDED',
evidenceRequirements: ['API-Security-Konzept', 'OAuth-Konfiguration', 'Rate-Limiting-Regeln'],
reviewFrequency: 'QUARTERLY',
priority: 'HIGH',
complexity: 'MEDIUM',
tags: ['authorization', 'api', 'oauth'],
},
{
id: 'TOM-AZ-07',
code: 'TOM-AZ-07',
category: 'ACCESS_AUTHORIZATION',
type: 'ORGANIZATIONAL',
name: { de: 'Regelmaessiger Berechtigungsreview', en: 'Regular Permission Review' },
description: {
de: 'Systematische Ueberpruefung und Bereinigung von Zugriffsberechtigungen in regelmaessigen Abstaenden durch die jeweiligen Fachverantwortlichen.',
en: 'Systematic review and cleanup of access permissions at regular intervals by the respective department heads.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. d' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.9.2.5' },
],
applicabilityConditions: [],
defaultApplicability: 'REQUIRED',
evidenceRequirements: ['Review-Protokolle', 'Berechtigungsaenderungslog', 'Freigabedokumentation'],
reviewFrequency: 'SEMI_ANNUAL',
priority: 'HIGH',
complexity: 'LOW',
tags: ['authorization', 'review', 'permissions'],
},
// TRANSFER_CONTROL — 2 new
{
id: 'TOM-TR-06',
code: 'TOM-TR-06',
category: 'TRANSFER_CONTROL',
type: 'TECHNICAL',
name: { de: 'E-Mail-Verschluesselung (erweitert)', en: 'Email Encryption (Extended)' },
description: {
de: 'Erweiterte E-Mail-Verschluesselung mit automatischer Erkennung sensibler Inhalte und erzwungener Gateway-Verschluesselung.',
en: 'Extended email encryption with automatic detection of sensitive content and enforced gateway encryption.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. a' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.13.2.3' },
],
applicabilityConditions: [
{ field: 'dataProfile.hasSpecialCategories', operator: 'EQUALS', value: true, result: 'REQUIRED', priority: 25 },
{ field: 'riskProfile.protectionLevel', operator: 'IN', value: ['HIGH', 'VERY_HIGH'], result: 'RECOMMENDED', priority: 15 },
],
defaultApplicability: 'OPTIONAL',
evidenceRequirements: ['E-Mail-Verschluesselungs-Policy', 'Gateway-Konfiguration', 'DLP-Regeln'],
reviewFrequency: 'SEMI_ANNUAL',
priority: 'MEDIUM',
complexity: 'MEDIUM',
tags: ['transfer', 'email', 'encryption'],
},
{
id: 'TOM-TR-07',
code: 'TOM-TR-07',
category: 'TRANSFER_CONTROL',
type: 'ORGANIZATIONAL',
name: { de: 'Drittstaat-Transferbewertung', en: 'Third Country Transfer Assessment' },
description: {
de: 'Dokumentierte Bewertung und Absicherung von Datenuebermittlungen in Drittstaaten gemaess Art. 44-49 DSGVO (Standardvertragsklauseln, TIA).',
en: 'Documented assessment and safeguarding of data transfers to third countries according to Art. 44-49 GDPR (Standard Contractual Clauses, TIA).',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 44-49' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.15.1.2' },
],
applicabilityConditions: [
{ field: 'dataProfile.thirdCountryTransfers', operator: 'EQUALS', value: true, result: 'REQUIRED', priority: 30 },
{ field: 'architectureProfile.hostingLocation', operator: 'IN', value: ['THIRD_COUNTRY_ADEQUATE', 'THIRD_COUNTRY'], result: 'REQUIRED', priority: 25 },
],
defaultApplicability: 'OPTIONAL',
evidenceRequirements: ['Transfer Impact Assessment', 'Standardvertragsklauseln', 'Angemessenheitsbeschluss-Pruefung'],
reviewFrequency: 'ANNUAL',
priority: 'CRITICAL',
complexity: 'MEDIUM',
tags: ['transfer', 'third-country', 'schrems-ii'],
},
// AVAILABILITY — 2 new
{
id: 'TOM-AV-06',
code: 'TOM-AV-06',
category: 'AVAILABILITY',
type: 'TECHNICAL',
name: { de: 'Monitoring und Alerting', en: 'Monitoring and Alerting' },
description: {
de: 'Implementierung einer umfassenden Ueberwachung aller IT-Systeme mit automatischen Benachrichtigungen bei Stoerungen oder Schwellenwert-Ueberschreitungen.',
en: 'Implementation of comprehensive monitoring of all IT systems with automatic notifications for disruptions or threshold violations.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. b' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.12.4.1' },
{ framework: 'BSI_IT_GRUNDSCHUTZ', reference: 'OPS.1.1.2' },
],
applicabilityConditions: [],
defaultApplicability: 'REQUIRED',
evidenceRequirements: ['Monitoring-Konzept', 'Alerting-Konfiguration', 'Eskalationsmatrix'],
reviewFrequency: 'QUARTERLY',
priority: 'HIGH',
complexity: 'MEDIUM',
tags: ['availability', 'monitoring', 'alerting'],
},
{
id: 'TOM-AV-07',
code: 'TOM-AV-07',
category: 'AVAILABILITY',
type: 'ORGANIZATIONAL',
name: { de: 'Service Level Management', en: 'Service Level Management' },
description: {
de: 'Definition und Ueberwachung von Service Level Agreements (SLAs) fuer alle kritischen IT-Services mit klaren Verfuegbarkeitszielen.',
en: 'Definition and monitoring of Service Level Agreements (SLAs) for all critical IT services with clear availability targets.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. b' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.15.2.1' },
],
applicabilityConditions: [
{ field: 'companyProfile.size', operator: 'IN', value: ['MEDIUM', 'LARGE', 'ENTERPRISE'], result: 'RECOMMENDED', priority: 10 },
{ field: 'architectureProfile.hasSubprocessors', operator: 'EQUALS', value: true, result: 'REQUIRED', priority: 20 },
],
defaultApplicability: 'RECOMMENDED',
evidenceRequirements: ['SLA-Dokumentation', 'Verfuegbarkeitsberichte', 'Eskalationsverfahren'],
reviewFrequency: 'QUARTERLY',
priority: 'MEDIUM',
complexity: 'LOW',
tags: ['availability', 'sla', 'service-management'],
},
// SEPARATION — 1 more new (TOM-DL-05)
{
id: 'TOM-DL-05',
code: 'TOM-DL-05',
category: 'SEPARATION',
type: 'ORGANIZATIONAL',
name: { de: 'Datenloesch-Audit', en: 'Data Deletion Audit' },
description: {
de: 'Regelmaessige Ueberpruefung der Wirksamkeit und Vollstaendigkeit von Datenloeschvorgaengen durch unabhaengige Stellen.',
en: 'Regular review of the effectiveness and completeness of data deletion processes by independent parties.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 5 Abs. 1 lit. e' },
{ framework: 'GDPR_ART32', reference: 'Art. 17' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.8.3.2' },
],
applicabilityConditions: [
{ field: 'dataProfile.hasSpecialCategories', operator: 'EQUALS', value: true, result: 'REQUIRED', priority: 25 },
],
defaultApplicability: 'RECOMMENDED',
evidenceRequirements: ['Audit-Berichte', 'Loeschprotokolle', 'Stichproben-Ergebnisse'],
reviewFrequency: 'ANNUAL',
priority: 'MEDIUM',
complexity: 'MEDIUM',
tags: ['separation', 'deletion', 'audit'],
},
// REVIEW — 3 new
{
id: 'TOM-RV-09',
code: 'TOM-RV-09',
category: 'REVIEW',
type: 'ORGANIZATIONAL',
name: { de: 'Datenschutz-Audit-Programm', en: 'Data Protection Audit Program' },
description: {
de: 'Systematisches Programm zur regelmaessigen internen Ueberpruefung aller Datenschutzmassnahmen mit dokumentierten Ergebnissen und Massnahmenverfolgung.',
en: 'Systematic program for regular internal review of all data protection measures with documented results and action tracking.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. d' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.18.2.1' },
{ framework: 'BSI_IT_GRUNDSCHUTZ', reference: 'DER.3.1' },
],
applicabilityConditions: [],
defaultApplicability: 'REQUIRED',
evidenceRequirements: ['Audit-Programm', 'Audit-Berichte', 'Massnahmenplan'],
reviewFrequency: 'ANNUAL',
priority: 'HIGH',
complexity: 'MEDIUM',
tags: ['review', 'audit', 'data-protection'],
},
{
id: 'TOM-RV-10',
code: 'TOM-RV-10',
category: 'REVIEW',
type: 'TECHNICAL',
name: { de: 'Automatisierte Compliance-Pruefung', en: 'Automated Compliance Checking' },
description: {
de: 'Einsatz automatisierter Tools zur kontinuierlichen Ueberpruefung der Einhaltung von Sicherheits- und Datenschutzrichtlinien.',
en: 'Use of automated tools for continuous monitoring of compliance with security and data protection policies.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. d' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.18.2.2' },
],
applicabilityConditions: [
{ field: 'companyProfile.size', operator: 'IN', value: ['MEDIUM', 'LARGE', 'ENTERPRISE'], result: 'RECOMMENDED', priority: 10 },
{ field: 'riskProfile.protectionLevel', operator: 'IN', value: ['HIGH', 'VERY_HIGH'], result: 'RECOMMENDED', priority: 15 },
],
defaultApplicability: 'OPTIONAL',
evidenceRequirements: ['Tool-Konfiguration', 'Compliance-Dashboard', 'Automatisierte Berichte'],
reviewFrequency: 'QUARTERLY',
priority: 'MEDIUM',
complexity: 'HIGH',
tags: ['review', 'automation', 'compliance'],
},
{
id: 'TOM-RV-11',
code: 'TOM-RV-11',
category: 'REVIEW',
type: 'ORGANIZATIONAL',
name: { de: 'Management Review (Art. 32 Abs. 1 lit. d)', en: 'Management Review (Art. 32(1)(d))' },
description: {
de: 'Regelmaessige Ueberpruefung der Wirksamkeit aller technischen und organisatorischen Massnahmen durch die Geschaeftsfuehrung mit dokumentierten Ergebnissen.',
en: 'Regular review of the effectiveness of all technical and organizational measures by management with documented results.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. d' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.18.2.1' },
],
applicabilityConditions: [],
defaultApplicability: 'REQUIRED',
evidenceRequirements: ['Management-Review-Protokolle', 'Massnahmenplan', 'Wirksamkeitsbewertung'],
reviewFrequency: 'ANNUAL',
priority: 'HIGH',
complexity: 'LOW',
tags: ['review', 'management', 'effectiveness'],
},
],
}