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:
@@ -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()
|
||||
}
|
||||
|
||||
449
admin-compliance/components/sdk/tom-dashboard/TOMDocumentTab.tsx
Normal file
449
admin-compliance/components/sdk/tom-dashboard/TOMDocumentTab.tsx
Normal 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 }
|
||||
@@ -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">
|
||||
|
||||
@@ -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 "Controls synchronisieren", 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">
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { TOMOverviewTab } from './TOMOverviewTab'
|
||||
export { TOMEditorTab } from './TOMEditorTab'
|
||||
export { TOMGapExportTab } from './TOMGapExportTab'
|
||||
export { TOMDocumentTab } from './TOMDocumentTab'
|
||||
|
||||
553
admin-compliance/lib/sdk/tom-compliance.ts
Normal file
553
admin-compliance/lib/sdk/tom-compliance.ts
Normal 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
906
admin-compliance/lib/sdk/tom-document.ts
Normal file
906
admin-compliance/lib/sdk/tom-document.ts
Normal 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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
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 '-'
|
||||
}
|
||||
}
|
||||
@@ -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'],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,8 @@ _ROUTER_MODULES = [
|
||||
"crosswalk_routes",
|
||||
"process_task_routes",
|
||||
"evidence_check_routes",
|
||||
"vvt_library_routes",
|
||||
"tom_mapping_routes",
|
||||
]
|
||||
|
||||
_loaded_count = 0
|
||||
|
||||
537
backend-compliance/compliance/api/tom_mapping_routes.py
Normal file
537
backend-compliance/compliance/api/tom_mapping_routes.py
Normal file
@@ -0,0 +1,537 @@
|
||||
"""
|
||||
TOM ↔ Canonical Control Mapping Routes.
|
||||
|
||||
Three-layer architecture:
|
||||
TOM Measures (~88, audit-level) → Mapping Bridge → Canonical Controls (10,000+)
|
||||
|
||||
Endpoints:
|
||||
POST /v1/tom-mappings/sync — Sync canonical controls for company profile
|
||||
GET /v1/tom-mappings — List all mappings for tenant/project
|
||||
GET /v1/tom-mappings/by-tom/{code} — Mappings for a specific TOM control
|
||||
GET /v1/tom-mappings/stats — Coverage statistics
|
||||
POST /v1/tom-mappings/manual — Manually add a mapping
|
||||
DELETE /v1/tom-mappings/{id} — Remove a mapping
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query, Header
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import text
|
||||
|
||||
from database import SessionLocal
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/tom-mappings", tags=["tom-control-mappings"])
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# TOM CATEGORY → CANONICAL CATEGORY MAPPING
|
||||
# =============================================================================
|
||||
|
||||
# Maps 13 TOM control categories to canonical_control_categories
|
||||
# Each TOM category maps to 1-3 canonical categories for broad coverage
|
||||
TOM_TO_CANONICAL_CATEGORIES: dict[str, list[str]] = {
|
||||
"ACCESS_CONTROL": ["authentication", "identity", "physical"],
|
||||
"ADMISSION_CONTROL": ["authentication", "identity", "system"],
|
||||
"ACCESS_AUTHORIZATION": ["authentication", "identity"],
|
||||
"TRANSFER_CONTROL": ["network", "data_protection", "encryption"],
|
||||
"INPUT_CONTROL": ["application", "data_protection"],
|
||||
"ORDER_CONTROL": ["supply_chain", "compliance"],
|
||||
"AVAILABILITY": ["continuity", "system"],
|
||||
"SEPARATION": ["network", "data_protection"],
|
||||
"ENCRYPTION": ["encryption"],
|
||||
"PSEUDONYMIZATION": ["data_protection", "encryption"],
|
||||
"RESILIENCE": ["continuity", "system"],
|
||||
"RECOVERY": ["continuity"],
|
||||
"REVIEW": ["compliance", "governance", "risk"],
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# REQUEST / RESPONSE MODELS
|
||||
# =============================================================================
|
||||
|
||||
class SyncRequest(BaseModel):
|
||||
"""Trigger a sync of canonical controls to TOM measures."""
|
||||
industry: Optional[str] = None
|
||||
company_size: Optional[str] = None
|
||||
force: bool = False
|
||||
|
||||
|
||||
class ManualMappingRequest(BaseModel):
|
||||
"""Manually add a canonical control to a TOM measure."""
|
||||
tom_control_code: str
|
||||
tom_category: str
|
||||
canonical_control_id: str
|
||||
canonical_control_code: str
|
||||
canonical_category: Optional[str] = None
|
||||
relevance_score: float = 1.0
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# HELPERS
|
||||
# =============================================================================
|
||||
|
||||
def _get_tenant_id(x_tenant_id: Optional[str]) -> str:
|
||||
"""Extract tenant ID from header."""
|
||||
if not x_tenant_id:
|
||||
raise HTTPException(status_code=400, detail="X-Tenant-ID header required")
|
||||
return x_tenant_id
|
||||
|
||||
|
||||
def _compute_profile_hash(industry: Optional[str], company_size: Optional[str]) -> str:
|
||||
"""Compute a hash from profile parameters for change detection."""
|
||||
data = json.dumps({"industry": industry, "company_size": company_size}, sort_keys=True)
|
||||
return hashlib.sha256(data.encode()).hexdigest()[:16]
|
||||
|
||||
|
||||
def _mapping_row_to_dict(r) -> dict[str, Any]:
|
||||
"""Convert a mapping row to API response dict."""
|
||||
return {
|
||||
"id": str(r.id),
|
||||
"tenant_id": str(r.tenant_id),
|
||||
"project_id": str(r.project_id) if r.project_id else None,
|
||||
"tom_control_code": r.tom_control_code,
|
||||
"tom_category": r.tom_category,
|
||||
"canonical_control_id": str(r.canonical_control_id),
|
||||
"canonical_control_code": r.canonical_control_code,
|
||||
"canonical_category": r.canonical_category,
|
||||
"mapping_type": r.mapping_type,
|
||||
"relevance_score": float(r.relevance_score) if r.relevance_score else 1.0,
|
||||
"created_at": r.created_at.isoformat() if r.created_at else None,
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# SYNC ENDPOINT
|
||||
# =============================================================================
|
||||
|
||||
@router.post("/sync")
|
||||
async def sync_mappings(
|
||||
body: SyncRequest,
|
||||
x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID"),
|
||||
project_id: Optional[str] = Query(None),
|
||||
):
|
||||
"""
|
||||
Sync canonical controls to TOM measures based on company profile.
|
||||
|
||||
Algorithm:
|
||||
1. Compute profile hash → skip if unchanged (unless force=True)
|
||||
2. For each TOM category, find matching canonical controls by:
|
||||
- Category mapping (TOM category → canonical categories)
|
||||
- Industry filter (applicable_industries JSONB containment)
|
||||
- Company size filter (applicable_company_size JSONB containment)
|
||||
- Only approved + customer_visible controls
|
||||
3. Delete old auto-mappings, insert new ones
|
||||
4. Update sync state
|
||||
"""
|
||||
tenant_id = _get_tenant_id(x_tenant_id)
|
||||
profile_hash = _compute_profile_hash(body.industry, body.company_size)
|
||||
|
||||
with SessionLocal() as db:
|
||||
# Check if sync is needed (profile unchanged)
|
||||
if not body.force:
|
||||
existing = db.execute(
|
||||
text("""
|
||||
SELECT profile_hash FROM tom_control_sync_state
|
||||
WHERE tenant_id = :tid AND (project_id = :pid OR (project_id IS NULL AND :pid IS NULL))
|
||||
"""),
|
||||
{"tid": tenant_id, "pid": project_id},
|
||||
).fetchone()
|
||||
if existing and existing.profile_hash == profile_hash:
|
||||
return {
|
||||
"status": "unchanged",
|
||||
"message": "Profile unchanged since last sync",
|
||||
"profile_hash": profile_hash,
|
||||
}
|
||||
|
||||
# Delete old auto-mappings for this tenant+project
|
||||
db.execute(
|
||||
text("""
|
||||
DELETE FROM tom_control_mappings
|
||||
WHERE tenant_id = :tid
|
||||
AND (project_id = :pid OR (project_id IS NULL AND :pid IS NULL))
|
||||
AND mapping_type = 'auto'
|
||||
"""),
|
||||
{"tid": tenant_id, "pid": project_id},
|
||||
)
|
||||
|
||||
total_mappings = 0
|
||||
canonical_ids_matched = set()
|
||||
tom_codes_covered = set()
|
||||
|
||||
# For each TOM category, find matching canonical controls
|
||||
for tom_category, canonical_categories in TOM_TO_CANONICAL_CATEGORIES.items():
|
||||
# Build JSONB containment query for categories
|
||||
cat_conditions = " OR ".join(
|
||||
f"category = :cat_{i}" for i in range(len(canonical_categories))
|
||||
)
|
||||
cat_params = {f"cat_{i}": c for i, c in enumerate(canonical_categories)}
|
||||
|
||||
# Build industry filter
|
||||
industry_filter = ""
|
||||
if body.industry:
|
||||
industry_filter = """
|
||||
AND (
|
||||
applicable_industries IS NULL
|
||||
OR applicable_industries @> '"all"'::jsonb
|
||||
OR applicable_industries @> (:industry)::jsonb
|
||||
)
|
||||
"""
|
||||
cat_params["industry"] = json.dumps([body.industry])
|
||||
|
||||
# Build company size filter
|
||||
size_filter = ""
|
||||
if body.company_size:
|
||||
size_filter = """
|
||||
AND (
|
||||
applicable_company_size IS NULL
|
||||
OR applicable_company_size @> '"all"'::jsonb
|
||||
OR applicable_company_size @> (:csize)::jsonb
|
||||
)
|
||||
"""
|
||||
cat_params["csize"] = json.dumps([body.company_size])
|
||||
|
||||
query = f"""
|
||||
SELECT id, control_id, category
|
||||
FROM canonical_controls
|
||||
WHERE ({cat_conditions})
|
||||
AND release_state = 'approved'
|
||||
AND customer_visible = true
|
||||
{industry_filter}
|
||||
{size_filter}
|
||||
ORDER BY control_id
|
||||
"""
|
||||
|
||||
rows = db.execute(text(query), cat_params).fetchall()
|
||||
|
||||
# Find TOM control codes in this category (query the frontend library
|
||||
# codes; we use the category prefix pattern from the loader)
|
||||
# TOM codes follow pattern: TOM-XX-NN where XX is category abbreviation
|
||||
# We insert one mapping per canonical control per TOM category
|
||||
for row in rows:
|
||||
db.execute(
|
||||
text("""
|
||||
INSERT INTO tom_control_mappings (
|
||||
tenant_id, project_id, tom_control_code, tom_category,
|
||||
canonical_control_id, canonical_control_code, canonical_category,
|
||||
mapping_type, relevance_score
|
||||
) VALUES (
|
||||
:tid, :pid, :tom_cat, :tom_cat,
|
||||
:cc_id, :cc_code, :cc_category,
|
||||
'auto', 1.00
|
||||
)
|
||||
ON CONFLICT (tenant_id, project_id, tom_control_code, canonical_control_id)
|
||||
DO NOTHING
|
||||
"""),
|
||||
{
|
||||
"tid": tenant_id,
|
||||
"pid": project_id,
|
||||
"tom_cat": tom_category,
|
||||
"cc_id": str(row.id),
|
||||
"cc_code": row.control_id,
|
||||
"cc_category": row.category,
|
||||
},
|
||||
)
|
||||
total_mappings += 1
|
||||
canonical_ids_matched.add(str(row.id))
|
||||
tom_codes_covered.add(tom_category)
|
||||
|
||||
# Upsert sync state
|
||||
db.execute(
|
||||
text("""
|
||||
INSERT INTO tom_control_sync_state (
|
||||
tenant_id, project_id, profile_hash,
|
||||
total_mappings, canonical_controls_matched, tom_controls_covered,
|
||||
last_synced_at
|
||||
) VALUES (
|
||||
:tid, :pid, :hash,
|
||||
:total, :matched, :covered,
|
||||
NOW()
|
||||
)
|
||||
ON CONFLICT (tenant_id, project_id)
|
||||
DO UPDATE SET
|
||||
profile_hash = :hash,
|
||||
total_mappings = :total,
|
||||
canonical_controls_matched = :matched,
|
||||
tom_controls_covered = :covered,
|
||||
last_synced_at = NOW()
|
||||
"""),
|
||||
{
|
||||
"tid": tenant_id,
|
||||
"pid": project_id,
|
||||
"hash": profile_hash,
|
||||
"total": total_mappings,
|
||||
"matched": len(canonical_ids_matched),
|
||||
"covered": len(tom_codes_covered),
|
||||
},
|
||||
)
|
||||
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"status": "synced",
|
||||
"profile_hash": profile_hash,
|
||||
"total_mappings": total_mappings,
|
||||
"canonical_controls_matched": len(canonical_ids_matched),
|
||||
"tom_categories_covered": len(tom_codes_covered),
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# LIST MAPPINGS
|
||||
# =============================================================================
|
||||
|
||||
@router.get("")
|
||||
async def list_mappings(
|
||||
x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID"),
|
||||
project_id: Optional[str] = Query(None),
|
||||
tom_category: Optional[str] = Query(None),
|
||||
mapping_type: Optional[str] = Query(None),
|
||||
limit: int = Query(500, ge=1, le=5000),
|
||||
offset: int = Query(0, ge=0),
|
||||
):
|
||||
"""List all TOM ↔ canonical control mappings for tenant/project."""
|
||||
tenant_id = _get_tenant_id(x_tenant_id)
|
||||
|
||||
query = """
|
||||
SELECT m.*, cc.title as canonical_title, cc.severity as canonical_severity
|
||||
FROM tom_control_mappings m
|
||||
LEFT JOIN canonical_controls cc ON cc.id = m.canonical_control_id
|
||||
WHERE m.tenant_id = :tid
|
||||
AND (m.project_id = :pid OR (m.project_id IS NULL AND :pid IS NULL))
|
||||
"""
|
||||
params: dict[str, Any] = {"tid": tenant_id, "pid": project_id}
|
||||
|
||||
if tom_category:
|
||||
query += " AND m.tom_category = :tcat"
|
||||
params["tcat"] = tom_category
|
||||
if mapping_type:
|
||||
query += " AND m.mapping_type = :mtype"
|
||||
params["mtype"] = mapping_type
|
||||
|
||||
query += " ORDER BY m.tom_category, m.canonical_control_code"
|
||||
query += " LIMIT :lim OFFSET :off"
|
||||
params["lim"] = limit
|
||||
params["off"] = offset
|
||||
|
||||
count_query = """
|
||||
SELECT count(*) FROM tom_control_mappings
|
||||
WHERE tenant_id = :tid
|
||||
AND (project_id = :pid OR (project_id IS NULL AND :pid IS NULL))
|
||||
"""
|
||||
count_params: dict[str, Any] = {"tid": tenant_id, "pid": project_id}
|
||||
if tom_category:
|
||||
count_query += " AND tom_category = :tcat"
|
||||
count_params["tcat"] = tom_category
|
||||
|
||||
with SessionLocal() as db:
|
||||
rows = db.execute(text(query), params).fetchall()
|
||||
total = db.execute(text(count_query), count_params).scalar()
|
||||
|
||||
mappings = []
|
||||
for r in rows:
|
||||
d = _mapping_row_to_dict(r)
|
||||
d["canonical_title"] = getattr(r, "canonical_title", None)
|
||||
d["canonical_severity"] = getattr(r, "canonical_severity", None)
|
||||
mappings.append(d)
|
||||
|
||||
return {"mappings": mappings, "total": total}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MAPPINGS BY TOM CONTROL
|
||||
# =============================================================================
|
||||
|
||||
@router.get("/by-tom/{tom_code}")
|
||||
async def get_mappings_by_tom(
|
||||
tom_code: str,
|
||||
x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID"),
|
||||
project_id: Optional[str] = Query(None),
|
||||
):
|
||||
"""Get all canonical controls mapped to a specific TOM control code or category."""
|
||||
tenant_id = _get_tenant_id(x_tenant_id)
|
||||
|
||||
with SessionLocal() as db:
|
||||
rows = db.execute(
|
||||
text("""
|
||||
SELECT m.*, cc.title as canonical_title, cc.severity as canonical_severity,
|
||||
cc.objective as canonical_objective
|
||||
FROM tom_control_mappings m
|
||||
LEFT JOIN canonical_controls cc ON cc.id = m.canonical_control_id
|
||||
WHERE m.tenant_id = :tid
|
||||
AND (m.project_id = :pid OR (m.project_id IS NULL AND :pid IS NULL))
|
||||
AND (m.tom_control_code = :code OR m.tom_category = :code)
|
||||
ORDER BY m.canonical_control_code
|
||||
"""),
|
||||
{"tid": tenant_id, "pid": project_id, "code": tom_code},
|
||||
).fetchall()
|
||||
|
||||
mappings = []
|
||||
for r in rows:
|
||||
d = _mapping_row_to_dict(r)
|
||||
d["canonical_title"] = getattr(r, "canonical_title", None)
|
||||
d["canonical_severity"] = getattr(r, "canonical_severity", None)
|
||||
d["canonical_objective"] = getattr(r, "canonical_objective", None)
|
||||
mappings.append(d)
|
||||
|
||||
return {"tom_code": tom_code, "mappings": mappings, "total": len(mappings)}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# STATS
|
||||
# =============================================================================
|
||||
|
||||
@router.get("/stats")
|
||||
async def get_mapping_stats(
|
||||
x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID"),
|
||||
project_id: Optional[str] = Query(None),
|
||||
):
|
||||
"""Coverage statistics for TOM ↔ canonical control mappings."""
|
||||
tenant_id = _get_tenant_id(x_tenant_id)
|
||||
|
||||
with SessionLocal() as db:
|
||||
# Sync state
|
||||
sync_state = db.execute(
|
||||
text("""
|
||||
SELECT * FROM tom_control_sync_state
|
||||
WHERE tenant_id = :tid
|
||||
AND (project_id = :pid OR (project_id IS NULL AND :pid IS NULL))
|
||||
"""),
|
||||
{"tid": tenant_id, "pid": project_id},
|
||||
).fetchone()
|
||||
|
||||
# Per-category breakdown
|
||||
category_stats = db.execute(
|
||||
text("""
|
||||
SELECT tom_category,
|
||||
count(*) as total_mappings,
|
||||
count(DISTINCT canonical_control_id) as unique_controls,
|
||||
count(*) FILTER (WHERE mapping_type = 'auto') as auto_count,
|
||||
count(*) FILTER (WHERE mapping_type = 'manual') as manual_count
|
||||
FROM tom_control_mappings
|
||||
WHERE tenant_id = :tid
|
||||
AND (project_id = :pid OR (project_id IS NULL AND :pid IS NULL))
|
||||
GROUP BY tom_category
|
||||
ORDER BY tom_category
|
||||
"""),
|
||||
{"tid": tenant_id, "pid": project_id},
|
||||
).fetchall()
|
||||
|
||||
# Total canonical controls in DB (approved + visible)
|
||||
total_canonical = db.execute(
|
||||
text("""
|
||||
SELECT count(*) FROM canonical_controls
|
||||
WHERE release_state = 'approved' AND customer_visible = true
|
||||
""")
|
||||
).scalar()
|
||||
|
||||
return {
|
||||
"sync_state": {
|
||||
"profile_hash": sync_state.profile_hash if sync_state else None,
|
||||
"total_mappings": sync_state.total_mappings if sync_state else 0,
|
||||
"canonical_controls_matched": sync_state.canonical_controls_matched if sync_state else 0,
|
||||
"tom_controls_covered": sync_state.tom_controls_covered if sync_state else 0,
|
||||
"last_synced_at": sync_state.last_synced_at.isoformat() if sync_state and sync_state.last_synced_at else None,
|
||||
},
|
||||
"category_breakdown": [
|
||||
{
|
||||
"tom_category": r.tom_category,
|
||||
"total_mappings": r.total_mappings,
|
||||
"unique_controls": r.unique_controls,
|
||||
"auto_count": r.auto_count,
|
||||
"manual_count": r.manual_count,
|
||||
}
|
||||
for r in category_stats
|
||||
],
|
||||
"total_canonical_controls_available": total_canonical or 0,
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MANUAL MAPPING
|
||||
# =============================================================================
|
||||
|
||||
@router.post("/manual", status_code=201)
|
||||
async def add_manual_mapping(
|
||||
body: ManualMappingRequest,
|
||||
x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID"),
|
||||
project_id: Optional[str] = Query(None),
|
||||
):
|
||||
"""Manually add a canonical control to a TOM measure."""
|
||||
tenant_id = _get_tenant_id(x_tenant_id)
|
||||
|
||||
with SessionLocal() as db:
|
||||
# Verify canonical control exists
|
||||
cc = db.execute(
|
||||
text("SELECT id, control_id, category FROM canonical_controls WHERE id = CAST(:cid AS uuid)"),
|
||||
{"cid": body.canonical_control_id},
|
||||
).fetchone()
|
||||
if not cc:
|
||||
raise HTTPException(status_code=404, detail="Canonical control not found")
|
||||
|
||||
try:
|
||||
row = db.execute(
|
||||
text("""
|
||||
INSERT INTO tom_control_mappings (
|
||||
tenant_id, project_id, tom_control_code, tom_category,
|
||||
canonical_control_id, canonical_control_code, canonical_category,
|
||||
mapping_type, relevance_score
|
||||
) VALUES (
|
||||
:tid, :pid, :tom_code, :tom_cat,
|
||||
CAST(:cc_id AS uuid), :cc_code, :cc_category,
|
||||
'manual', :score
|
||||
)
|
||||
RETURNING *
|
||||
"""),
|
||||
{
|
||||
"tid": tenant_id,
|
||||
"pid": project_id,
|
||||
"tom_code": body.tom_control_code,
|
||||
"tom_cat": body.tom_category,
|
||||
"cc_id": body.canonical_control_id,
|
||||
"cc_code": body.canonical_control_code,
|
||||
"cc_category": body.canonical_category or cc.category,
|
||||
"score": body.relevance_score,
|
||||
},
|
||||
).fetchone()
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
if "unique" in str(e).lower() or "duplicate" in str(e).lower():
|
||||
raise HTTPException(status_code=409, detail="Mapping already exists")
|
||||
raise
|
||||
|
||||
return _mapping_row_to_dict(row)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DELETE MAPPING
|
||||
# =============================================================================
|
||||
|
||||
@router.delete("/{mapping_id}", status_code=204)
|
||||
async def delete_mapping(
|
||||
mapping_id: str,
|
||||
x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID"),
|
||||
):
|
||||
"""Remove a mapping (manual or auto)."""
|
||||
tenant_id = _get_tenant_id(x_tenant_id)
|
||||
|
||||
with SessionLocal() as db:
|
||||
result = db.execute(
|
||||
text("""
|
||||
DELETE FROM tom_control_mappings
|
||||
WHERE id = CAST(:mid AS uuid) AND tenant_id = :tid
|
||||
"""),
|
||||
{"mid": mapping_id, "tid": tenant_id},
|
||||
)
|
||||
if result.rowcount == 0:
|
||||
raise HTTPException(status_code=404, detail="Mapping not found")
|
||||
db.commit()
|
||||
|
||||
return None
|
||||
65
backend-compliance/migrations/068_tom_control_mappings.sql
Normal file
65
backend-compliance/migrations/068_tom_control_mappings.sql
Normal file
@@ -0,0 +1,65 @@
|
||||
-- Migration 068: TOM ↔ Canonical Control Mappings
|
||||
-- Bridge table connecting TOM measures (88) to Canonical Controls (10,000+)
|
||||
-- Enables three-layer architecture: TOM → Mapping → Canonical Controls
|
||||
|
||||
-- ============================================================================
|
||||
-- 1. Mapping table (TOM control code → Canonical control)
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tom_control_mappings (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
project_id UUID,
|
||||
|
||||
-- TOM side (references the embedded TOM control code, e.g. 'TOM-AC-01')
|
||||
tom_control_code VARCHAR(20) NOT NULL,
|
||||
tom_category VARCHAR(50) NOT NULL,
|
||||
|
||||
-- Canonical control side
|
||||
canonical_control_id UUID NOT NULL,
|
||||
canonical_control_code VARCHAR(20) NOT NULL,
|
||||
canonical_category VARCHAR(50),
|
||||
|
||||
-- Mapping metadata
|
||||
mapping_type VARCHAR(20) NOT NULL DEFAULT 'auto'
|
||||
CHECK (mapping_type IN ('auto', 'manual')),
|
||||
relevance_score NUMERIC(3,2) DEFAULT 1.00
|
||||
CHECK (relevance_score >= 0 AND relevance_score <= 1),
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
-- No duplicate mappings per tenant+project+TOM+canonical
|
||||
UNIQUE (tenant_id, project_id, tom_control_code, canonical_control_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tcm_tenant_project
|
||||
ON tom_control_mappings (tenant_id, project_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tcm_tom_code
|
||||
ON tom_control_mappings (tom_control_code);
|
||||
CREATE INDEX IF NOT EXISTS idx_tcm_canonical_id
|
||||
ON tom_control_mappings (canonical_control_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tcm_tom_category
|
||||
ON tom_control_mappings (tom_category);
|
||||
|
||||
-- ============================================================================
|
||||
-- 2. Sync state (tracks when the last sync ran + profile hash)
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tom_control_sync_state (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
project_id UUID,
|
||||
|
||||
-- Profile hash to detect changes (SHA-256 of serialized company profile)
|
||||
profile_hash VARCHAR(64),
|
||||
|
||||
-- Stats from last sync
|
||||
total_mappings INTEGER DEFAULT 0,
|
||||
canonical_controls_matched INTEGER DEFAULT 0,
|
||||
tom_controls_covered INTEGER DEFAULT 0,
|
||||
|
||||
last_synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
-- One sync state per tenant+project
|
||||
UNIQUE (tenant_id, project_id)
|
||||
);
|
||||
274
backend-compliance/tests/test_tom_mapping_routes.py
Normal file
274
backend-compliance/tests/test_tom_mapping_routes.py
Normal file
@@ -0,0 +1,274 @@
|
||||
"""
|
||||
Tests for TOM ↔ Canonical Control Mapping Routes.
|
||||
|
||||
Tests the three-layer architecture:
|
||||
TOM Measures → Mapping Bridge → Canonical Controls
|
||||
"""
|
||||
|
||||
import uuid
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from compliance.api.tom_mapping_routes import (
|
||||
router,
|
||||
TOM_TO_CANONICAL_CATEGORIES,
|
||||
_compute_profile_hash,
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# FIXTURES
|
||||
# =============================================================================
|
||||
|
||||
@pytest.fixture
|
||||
def app():
|
||||
"""Create a test FastAPI app with the TOM mapping router."""
|
||||
from fastapi import FastAPI
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
return app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(app):
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
TENANT_ID = "9282a473-5c95-4b3a-bf78-0ecc0ec71d3e"
|
||||
PROJECT_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
||||
HEADERS = {"X-Tenant-ID": TENANT_ID}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# UNIT TESTS
|
||||
# =============================================================================
|
||||
|
||||
class TestCategoryMapping:
|
||||
"""Test the TOM → Canonical category mapping dictionary."""
|
||||
|
||||
def test_all_13_tom_categories_mapped(self):
|
||||
expected = {
|
||||
"ACCESS_CONTROL", "ADMISSION_CONTROL", "ACCESS_AUTHORIZATION",
|
||||
"TRANSFER_CONTROL", "INPUT_CONTROL", "ORDER_CONTROL",
|
||||
"AVAILABILITY", "SEPARATION", "ENCRYPTION", "PSEUDONYMIZATION",
|
||||
"RESILIENCE", "RECOVERY", "REVIEW",
|
||||
}
|
||||
assert set(TOM_TO_CANONICAL_CATEGORIES.keys()) == expected
|
||||
|
||||
def test_each_category_has_at_least_one_canonical(self):
|
||||
for tom_cat, canonical_cats in TOM_TO_CANONICAL_CATEGORIES.items():
|
||||
assert len(canonical_cats) >= 1, f"{tom_cat} has no canonical categories"
|
||||
|
||||
def test_canonical_categories_are_valid(self):
|
||||
"""All referenced canonical categories must exist in the DB seed (migration 047)."""
|
||||
valid_canonical = {
|
||||
"encryption", "authentication", "network", "data_protection",
|
||||
"logging", "incident", "continuity", "compliance", "supply_chain",
|
||||
"physical", "personnel", "application", "system", "risk",
|
||||
"governance", "hardware", "identity",
|
||||
}
|
||||
for tom_cat, canonical_cats in TOM_TO_CANONICAL_CATEGORIES.items():
|
||||
for cc in canonical_cats:
|
||||
assert cc in valid_canonical, f"Invalid canonical category '{cc}' in {tom_cat}"
|
||||
|
||||
|
||||
class TestProfileHash:
|
||||
"""Test profile hash computation."""
|
||||
|
||||
def test_same_input_same_hash(self):
|
||||
h1 = _compute_profile_hash("Telekommunikation", "medium")
|
||||
h2 = _compute_profile_hash("Telekommunikation", "medium")
|
||||
assert h1 == h2
|
||||
|
||||
def test_different_input_different_hash(self):
|
||||
h1 = _compute_profile_hash("Telekommunikation", "medium")
|
||||
h2 = _compute_profile_hash("Gesundheitswesen", "large")
|
||||
assert h1 != h2
|
||||
|
||||
def test_none_values_produce_hash(self):
|
||||
h = _compute_profile_hash(None, None)
|
||||
assert len(h) == 16
|
||||
|
||||
def test_hash_is_16_chars(self):
|
||||
h = _compute_profile_hash("test", "small")
|
||||
assert len(h) == 16
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# API ENDPOINT TESTS (with mocked DB)
|
||||
# =============================================================================
|
||||
|
||||
class TestSyncEndpoint:
|
||||
"""Test POST /tom-mappings/sync."""
|
||||
|
||||
def test_sync_requires_tenant_header(self, client):
|
||||
resp = client.post("/tom-mappings/sync", json={"industry": "IT"})
|
||||
assert resp.status_code == 400
|
||||
assert "X-Tenant-ID" in resp.json()["detail"]
|
||||
|
||||
@patch("compliance.api.tom_mapping_routes.SessionLocal")
|
||||
def test_sync_unchanged_profile_skips(self, mock_session_cls, client):
|
||||
"""When profile hash matches, sync should return 'unchanged'."""
|
||||
mock_db = MagicMock()
|
||||
mock_session_cls.return_value.__enter__ = MagicMock(return_value=mock_db)
|
||||
mock_session_cls.return_value.__exit__ = MagicMock(return_value=False)
|
||||
|
||||
profile_hash = _compute_profile_hash("IT", "medium")
|
||||
mock_row = MagicMock()
|
||||
mock_row.profile_hash = profile_hash
|
||||
mock_db.execute.return_value.fetchone.return_value = mock_row
|
||||
|
||||
resp = client.post(
|
||||
"/tom-mappings/sync",
|
||||
json={"industry": "IT", "company_size": "medium"},
|
||||
headers=HEADERS,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["status"] == "unchanged"
|
||||
|
||||
@patch("compliance.api.tom_mapping_routes.SessionLocal")
|
||||
def test_sync_force_ignores_hash(self, mock_session_cls, client):
|
||||
"""force=True should sync even if hash matches."""
|
||||
mock_db = MagicMock()
|
||||
mock_session_cls.return_value.__enter__ = MagicMock(return_value=mock_db)
|
||||
mock_session_cls.return_value.__exit__ = MagicMock(return_value=False)
|
||||
|
||||
# Return empty results for canonical control queries
|
||||
mock_db.execute.return_value.fetchall.return_value = []
|
||||
mock_db.execute.return_value.fetchone.return_value = None
|
||||
|
||||
resp = client.post(
|
||||
"/tom-mappings/sync",
|
||||
json={"industry": "IT", "company_size": "medium", "force": True},
|
||||
headers=HEADERS,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["status"] == "synced"
|
||||
|
||||
|
||||
class TestListEndpoint:
|
||||
"""Test GET /tom-mappings."""
|
||||
|
||||
def test_list_requires_tenant_header(self, client):
|
||||
resp = client.get("/tom-mappings")
|
||||
assert resp.status_code == 400
|
||||
|
||||
@patch("compliance.api.tom_mapping_routes.SessionLocal")
|
||||
def test_list_returns_mappings(self, mock_session_cls, client):
|
||||
mock_db = MagicMock()
|
||||
mock_session_cls.return_value.__enter__ = MagicMock(return_value=mock_db)
|
||||
mock_session_cls.return_value.__exit__ = MagicMock(return_value=False)
|
||||
|
||||
mock_db.execute.return_value.fetchall.return_value = []
|
||||
mock_db.execute.return_value.scalar.return_value = 0
|
||||
|
||||
resp = client.get("/tom-mappings", headers=HEADERS)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert "mappings" in data
|
||||
assert "total" in data
|
||||
|
||||
|
||||
class TestByTomEndpoint:
|
||||
"""Test GET /tom-mappings/by-tom/{code}."""
|
||||
|
||||
def test_by_tom_requires_tenant_header(self, client):
|
||||
resp = client.get("/tom-mappings/by-tom/ENCRYPTION")
|
||||
assert resp.status_code == 400
|
||||
|
||||
@patch("compliance.api.tom_mapping_routes.SessionLocal")
|
||||
def test_by_tom_returns_mappings(self, mock_session_cls, client):
|
||||
mock_db = MagicMock()
|
||||
mock_session_cls.return_value.__enter__ = MagicMock(return_value=mock_db)
|
||||
mock_session_cls.return_value.__exit__ = MagicMock(return_value=False)
|
||||
|
||||
mock_db.execute.return_value.fetchall.return_value = []
|
||||
|
||||
resp = client.get("/tom-mappings/by-tom/ENCRYPTION", headers=HEADERS)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["tom_code"] == "ENCRYPTION"
|
||||
assert "mappings" in data
|
||||
|
||||
|
||||
class TestStatsEndpoint:
|
||||
"""Test GET /tom-mappings/stats."""
|
||||
|
||||
def test_stats_requires_tenant_header(self, client):
|
||||
resp = client.get("/tom-mappings/stats")
|
||||
assert resp.status_code == 400
|
||||
|
||||
@patch("compliance.api.tom_mapping_routes.SessionLocal")
|
||||
def test_stats_returns_structure(self, mock_session_cls, client):
|
||||
mock_db = MagicMock()
|
||||
mock_session_cls.return_value.__enter__ = MagicMock(return_value=mock_db)
|
||||
mock_session_cls.return_value.__exit__ = MagicMock(return_value=False)
|
||||
|
||||
mock_db.execute.return_value.fetchone.return_value = None
|
||||
mock_db.execute.return_value.fetchall.return_value = []
|
||||
mock_db.execute.return_value.scalar.return_value = 0
|
||||
|
||||
resp = client.get("/tom-mappings/stats", headers=HEADERS)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert "sync_state" in data
|
||||
assert "category_breakdown" in data
|
||||
assert "total_canonical_controls_available" in data
|
||||
|
||||
|
||||
class TestManualMappingEndpoint:
|
||||
"""Test POST /tom-mappings/manual."""
|
||||
|
||||
def test_manual_requires_tenant_header(self, client):
|
||||
resp = client.post("/tom-mappings/manual", json={
|
||||
"tom_control_code": "TOM-ENC-01",
|
||||
"tom_category": "ENCRYPTION",
|
||||
"canonical_control_id": str(uuid.uuid4()),
|
||||
"canonical_control_code": "CRYP-001",
|
||||
})
|
||||
assert resp.status_code == 400
|
||||
|
||||
@patch("compliance.api.tom_mapping_routes.SessionLocal")
|
||||
def test_manual_404_if_canonical_not_found(self, mock_session_cls, client):
|
||||
mock_db = MagicMock()
|
||||
mock_session_cls.return_value.__enter__ = MagicMock(return_value=mock_db)
|
||||
mock_session_cls.return_value.__exit__ = MagicMock(return_value=False)
|
||||
|
||||
mock_db.execute.return_value.fetchone.return_value = None
|
||||
|
||||
resp = client.post(
|
||||
"/tom-mappings/manual",
|
||||
json={
|
||||
"tom_control_code": "TOM-ENC-01",
|
||||
"tom_category": "ENCRYPTION",
|
||||
"canonical_control_id": str(uuid.uuid4()),
|
||||
"canonical_control_code": "CRYP-001",
|
||||
},
|
||||
headers=HEADERS,
|
||||
)
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
class TestDeleteMappingEndpoint:
|
||||
"""Test DELETE /tom-mappings/{id}."""
|
||||
|
||||
def test_delete_requires_tenant_header(self, client):
|
||||
resp = client.delete(f"/tom-mappings/{uuid.uuid4()}")
|
||||
assert resp.status_code == 400
|
||||
|
||||
@patch("compliance.api.tom_mapping_routes.SessionLocal")
|
||||
def test_delete_404_if_not_found(self, mock_session_cls, client):
|
||||
mock_db = MagicMock()
|
||||
mock_session_cls.return_value.__enter__ = MagicMock(return_value=mock_db)
|
||||
mock_session_cls.return_value.__exit__ = MagicMock(return_value=False)
|
||||
|
||||
mock_db.execute.return_value.rowcount = 0
|
||||
|
||||
resp = client.delete(
|
||||
f"/tom-mappings/{uuid.uuid4()}",
|
||||
headers=HEADERS,
|
||||
)
|
||||
assert resp.status_code == 404
|
||||
271
docs-src/services/sdk-modules/tom.md
Normal file
271
docs-src/services/sdk-modules/tom.md
Normal file
@@ -0,0 +1,271 @@
|
||||
# TOM — Technische und Organisatorische Massnahmen (Art. 32 DSGVO)
|
||||
|
||||
## Uebersicht
|
||||
|
||||
Das TOM-Modul implementiert die systematische Ableitung, Dokumentation und Ueberpruefung technischer und organisatorischer Massnahmen gemaess Art. 32 DSGVO. Es bietet einen 6-Schritt-Generator-Wizard, eine regelbasierte Kontrollbibliothek mit 88 Massnahmen, Gap-Analyse, SDM-Mapping, auditfaehige Dokumentengenerierung und 11 Compliance-Checks.
|
||||
|
||||
**Route:** `/sdk/tom` | **Backend:** `backend-compliance:8002` | **Checkpoint:** `CP-TOM`
|
||||
|
||||
---
|
||||
|
||||
## Art. 32 DSGVO Anforderungen
|
||||
|
||||
| Absatz | Anforderung | TOM-Modul Umsetzung |
|
||||
|--------|-------------|---------------------|
|
||||
| Art. 32 Abs. 1 lit. a | Pseudonymisierung und Verschluesselung | Kategorien ENCRYPTION (5 Controls) und PSEUDONYMIZATION (4 Controls) |
|
||||
| Art. 32 Abs. 1 lit. b | Vertraulichkeit, Integritaet, Verfuegbarkeit, Belastbarkeit | 8 Kategorien: ACCESS_CONTROL, ADMISSION_CONTROL, ACCESS_AUTHORIZATION, TRANSFER_CONTROL, RESILIENCE, AVAILABILITY, SEPARATION, INPUT_CONTROL |
|
||||
| Art. 32 Abs. 1 lit. c | Rasche Wiederherstellung nach Zwischenfall | Kategorie RECOVERY (5 Controls) |
|
||||
| Art. 32 Abs. 1 lit. d | Regelmaessige Ueberpruefung und Bewertung | Kategorie REVIEW (11 Controls) + Compliance-Check NO_REVIEW_PROCESS |
|
||||
|
||||
---
|
||||
|
||||
## TOM-Ableitung — Zwei Quellen
|
||||
|
||||
TOMs werden aus zwei unabhaengigen Quellen abgeleitet:
|
||||
|
||||
### 1. Scope/Profil-Module (Embedded Controls)
|
||||
|
||||
Der 6-Schritt-Wizard erfasst:
|
||||
|
||||
- **CompanyProfile**: Branche, Groesse, Rolle (Controller/Processor)
|
||||
- **DataProfile**: Datenkategorien, besondere Kategorien (Art. 9), Betroffene
|
||||
- **ArchitectureProfile**: Hosting, Cloud-Provider, Mandantenfaehigkeit
|
||||
- **SecurityProfile**: Auth, Backup, Logging, DR-Plan
|
||||
- **RiskProfile**: CIA-Bewertung, Schutzniveau
|
||||
|
||||
Die **Rules Engine** wertet 88 Embedded Controls gegen die Profile-Daten aus. Jeder Control hat `applicabilityConditions` mit Operatoren (EQUALS, IN, GREATER_THAN, CONTAINS) und Prioritaeten.
|
||||
|
||||
### 2. Canonical Control Library (CP-CLIB)
|
||||
|
||||
Die dynamisch generierte **Canonical Control Library** (`/sdk/control-library`) enthaelt unternehmensrelevante Security-Controls aus OWASP, NIST, ENISA und weiteren Frameworks. Diese werden durch den Control Generator Pipeline erzeugt und haben einen eigenen Review-Workflow.
|
||||
|
||||
!!! info "Herkunftsdokumentation"
|
||||
Im TOM-Dokument (Sektion 2 und 6) wird die **Herkunft** jeder Massnahme dokumentiert —
|
||||
ob sie aus der Embedded Control Library oder der Canonical Control Library stammt.
|
||||
|
||||
---
|
||||
|
||||
## Frontend — 5-Tab-Aufbau
|
||||
|
||||
| Tab | Beschreibung |
|
||||
|-----|-------------|
|
||||
| **Uebersicht** | Alle TOMs mit Filter (Kategorie, Typ, Status, Applicability), SDM-Abdeckung, Statistiken |
|
||||
| **Detail-Editor** | Einzelne TOM bearbeiten: Status, Verantwortlich, Evidence, Review-Datum |
|
||||
| **Generator** | 6-Schritt-Wizard starten, Quick-Stats |
|
||||
| **Gap-Analyse & Export** | Gap-Analyse-Ergebnisse, SDM/Modul-Abdeckung, JSON/DOCX Export |
|
||||
| **TOM-Dokument** | Auditfaehiges HTML-Dokument (12 Sektionen), Org-Header, Revisionsmanager, PDF-Druck |
|
||||
|
||||
---
|
||||
|
||||
## Kontrollbibliothek (88 Massnahmen)
|
||||
|
||||
| Kategorie | Code-Prefix | Anzahl | DSGVO-Referenz |
|
||||
|-----------|-------------|--------|----------------|
|
||||
| Zutrittskontrolle | TOM-AC | 6 | Art. 32 Abs. 1 lit. b |
|
||||
| Zugangskontrolle | TOM-ADM | 6 | Art. 32 Abs. 1 lit. b |
|
||||
| Zugriffskontrolle | TOM-AZ | 7 | Art. 32 Abs. 1 lit. b |
|
||||
| Weitergabekontrolle | TOM-TR | 7 | Art. 32 Abs. 1 lit. b |
|
||||
| Eingabekontrolle | TOM-IN | 5 | Art. 32 Abs. 1 lit. b |
|
||||
| Auftragskontrolle | TOM-OR | 6 | Art. 28 |
|
||||
| Verfuegbarkeit | TOM-AV | 7 | Art. 32 Abs. 1 lit. b, c |
|
||||
| Trennbarkeit | TOM-SE | 6 | Art. 32 Abs. 1 lit. b |
|
||||
| Verschluesselung | TOM-ENC | 5 | Art. 32 Abs. 1 lit. a |
|
||||
| Pseudonymisierung | TOM-PS | 4 | Art. 32 Abs. 1 lit. a |
|
||||
| Belastbarkeit | TOM-RE | 5 | Art. 32 Abs. 1 lit. b |
|
||||
| Wiederherstellbarkeit | TOM-RC | 5 | Art. 32 Abs. 1 lit. c |
|
||||
| Ueberpruefung & Bewertung | TOM-RV / TOM-DL / TOM-TR | 11 | Art. 32 Abs. 1 lit. d |
|
||||
|
||||
---
|
||||
|
||||
## SDM Gewaehrleistungsziele
|
||||
|
||||
Das [Standard-Datenschutzmodell](https://www.datenschutz-wiki.de/SDM) definiert 7 Gewaehrleistungsziele:
|
||||
|
||||
| Ziel | Relevante Kategorien |
|
||||
|------|---------------------|
|
||||
| Verfuegbarkeit | AVAILABILITY, RESILIENCE, RECOVERY |
|
||||
| Integritaet | ADMISSION_CONTROL, TRANSFER_CONTROL, INPUT_CONTROL, ENCRYPTION, RECOVERY |
|
||||
| Vertraulichkeit | ACCESS_CONTROL, ADMISSION_CONTROL, ACCESS_AUTHORIZATION, TRANSFER_CONTROL, ENCRYPTION |
|
||||
| Nichtverkettung | ACCESS_AUTHORIZATION, SEPARATION, PSEUDONYMIZATION |
|
||||
| Intervenierbarkeit | ORDER_CONTROL, REVIEW |
|
||||
| Transparenz | INPUT_CONTROL, ORDER_CONTROL, REVIEW |
|
||||
| Datenminimierung | SEPARATION, PSEUDONYMIZATION |
|
||||
|
||||
!!! tip "SDM-Abdeckung"
|
||||
Die Gap-Analyse (Tab 4) zeigt pro SDM-Gewaehrleistungsziel den Abdeckungsgrad
|
||||
in Prozent. Ziele mit 0% Abdeckung loesen den Compliance-Check `UNCOVERED_SDM_GOAL`
|
||||
(Schweregrad HIGH) aus.
|
||||
|
||||
---
|
||||
|
||||
## 11 Compliance-Checks
|
||||
|
||||
| # | Check | Schweregrad | Ausloeser |
|
||||
|---|-------|-------------|-----------|
|
||||
| 1 | `MISSING_RESPONSIBLE` | MEDIUM | REQUIRED-TOM ohne verantwortliche Person/Abteilung |
|
||||
| 2 | `OVERDUE_REVIEW` | MEDIUM | TOM mit reviewDate in der Vergangenheit |
|
||||
| 3 | `MISSING_EVIDENCE` | HIGH | IMPLEMENTED-TOM ohne Evidence (obwohl evidenceRequirements > 0) |
|
||||
| 4 | `INCOMPLETE_CATEGORY` | HIGH | Kategorie, in der alle REQUIRED-Controls NOT_IMPLEMENTED sind |
|
||||
| 5 | `NO_ENCRYPTION_MEASURES` | CRITICAL | Kein ENCRYPTION-Control implementiert |
|
||||
| 6 | `NO_PSEUDONYMIZATION` | MEDIUM | Besondere Datenkategorien (Art. 9) ohne PSEUDONYMIZATION |
|
||||
| 7 | `MISSING_AVAILABILITY` | HIGH | Kein AVAILABILITY/RECOVERY implementiert + kein DR-Plan |
|
||||
| 8 | `NO_REVIEW_PROCESS` | MEDIUM | Kein REVIEW-Control implementiert |
|
||||
| 9 | `UNCOVERED_SDM_GOAL` | HIGH | SDM-Gewaehrleistungsziel mit 0% Abdeckung |
|
||||
| 10 | `HIGH_RISK_WITHOUT_MEASURES` | CRITICAL | Schutzniveau VERY_HIGH aber < 50% implementiert |
|
||||
| 11 | `STALE_NOT_IMPLEMENTED` | LOW | REQUIRED-TOM seit > 90 Tagen NOT_IMPLEMENTED |
|
||||
|
||||
**Score-Berechnung:** `100 - (CRITICAL*15 + HIGH*10 + MEDIUM*5 + LOW*2)`, Minimum 0.
|
||||
|
||||
---
|
||||
|
||||
## Backend API (9+ Endpoints)
|
||||
|
||||
| Methode | Pfad | Beschreibung |
|
||||
|---------|------|--------------|
|
||||
| `GET` | `/api/v1/tom/state` | TOM-Generator-State laden |
|
||||
| `POST` | `/api/v1/tom/state` | State speichern |
|
||||
| `POST` | `/api/v1/tom/evaluate` | Controls evaluieren (Rules Engine) |
|
||||
| `POST` | `/api/v1/tom/gap-analysis` | Gap-Analyse durchfuehren |
|
||||
| `POST` | `/api/v1/tom/evidence/upload` | Evidence-Dokument hochladen |
|
||||
| `POST` | `/api/v1/tom/evidence/analyze` | KI-Analyse eines Evidence-Dokuments |
|
||||
| `POST` | `/api/v1/tom/export` | Export (JSON, DOCX, PDF, ZIP) |
|
||||
| `GET` | `/api/v1/tom/controls` | Kontrollbibliothek abrufen |
|
||||
| `GET` | `/api/v1/tom/controls/:id` | Einzelnen Control abrufen |
|
||||
|
||||
### TOM ↔ Canonical Control Mapping API
|
||||
|
||||
| Methode | Pfad | Beschreibung |
|
||||
|---------|------|--------------|
|
||||
| `POST` | `/api/compliance/tom-mappings/sync` | Controls synchronisieren (Profil-basiert) |
|
||||
| `GET` | `/api/compliance/tom-mappings` | Alle Mappings auflisten |
|
||||
| `GET` | `/api/compliance/tom-mappings/by-tom/{code}` | Mappings pro TOM-Kategorie |
|
||||
| `GET` | `/api/compliance/tom-mappings/stats` | Coverage-Statistiken |
|
||||
| `POST` | `/api/compliance/tom-mappings/manual` | Manuelle Zuordnung |
|
||||
| `DELETE` | `/api/compliance/tom-mappings/{id}` | Zuordnung entfernen |
|
||||
|
||||
!!! info "Proxy-Route (Frontend)"
|
||||
Das Admin-Frontend ruft die Endpoints ueber den Next.js-Proxy auf:
|
||||
`/api/sdk/v1/tom/**` → `backend-compliance:8002/api/v1/tom/**`
|
||||
`/api/sdk/v1/compliance/tom-mappings/**` → `backend-compliance:8002/api/compliance/tom-mappings/**`
|
||||
|
||||
---
|
||||
|
||||
## TOM-Dokument (12 Sektionen)
|
||||
|
||||
| # | Sektion | Inhalt |
|
||||
|---|---------|--------|
|
||||
| 0 | Deckblatt | Organisation, DSB, IT-Sicherheit, Version |
|
||||
| — | Inhaltsverzeichnis | Automatisch generiert |
|
||||
| 1 | Ziel und Zweck | Art. 32 DSGVO Rechtsrahmen |
|
||||
| 2 | Geltungsbereich | Unternehmen, Hosting, Systeme, Control-Quellen |
|
||||
| 3 | Grundprinzipien Art. 32 | Vertraulichkeit, Integritaet, Verfuegbarkeit, Belastbarkeit, Wirksamkeitspruefung |
|
||||
| 4 | Schutzbedarf und Risikoanalyse | CIA-Bewertung, Schutzniveau, DSFA-Pflicht |
|
||||
| 5 | Massnahmen-Uebersicht | Tabelle: Kategorie, Anzahl, Status-Verteilung |
|
||||
| 6 | Detaillierte Massnahmen | Pro Kategorie: Detail-Karten mit Code, Status, Evidence, Mappings |
|
||||
| 7 | SDM Gewaehrleistungsziele | Abdeckungstabelle (7 Ziele x Prozent) |
|
||||
| 8 | Verantwortlichkeiten | Rollenmatrix |
|
||||
| 9 | Pruef- und Revisionszyklus | Review-Zeitplan |
|
||||
| 10 | Compliance-Status | Score, Issues nach Schweregrad |
|
||||
| 11 | Aenderungshistorie | Versionstabelle |
|
||||
|
||||
**Ausgabe:** HTML-Download oder PDF-Druck via Browser (`window.print()`).
|
||||
|
||||
---
|
||||
|
||||
## Drei-Schichten-Architektur (TOM ↔ Canonical Controls)
|
||||
|
||||
Die Audit-faehige Dokumentation nutzt eine **Drei-Schichten-Architektur**:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
TOM["TOM-Massnahmen (~88, Audit-Level)"]
|
||||
MAP["tom_control_mappings (Bridge-Tabelle)"]
|
||||
CC["Canonical Controls (10.000+, Implementation-Level)"]
|
||||
TOM --> MAP --> CC
|
||||
```
|
||||
|
||||
| Schicht | Beschreibung | Beispiel |
|
||||
|---------|-------------|---------|
|
||||
| **TOM-Massnahmen** | 88 abstrakte, auditfaehige Massnahmen in 13 Kategorien | TOM-ENC-01: Transportverschluesselung |
|
||||
| **Mapping-Bridge** | Verknuepfung TOM-Kategorie → Canonical Controls per Profil | ENCRYPTION → encryption-Kategorie, Industry-Filter |
|
||||
| **Canonical Controls** | 10.000+ konkrete Security-Controls aus OWASP, NIST, ENISA | CRYP-001: Cryptographic Key Lifecycle |
|
||||
|
||||
### Kategorie-Zuordnung
|
||||
|
||||
| TOM-Kategorie | Canonical Kategorien |
|
||||
|---------------|---------------------|
|
||||
| ACCESS_CONTROL | authentication, identity, physical |
|
||||
| ADMISSION_CONTROL | authentication, identity, system |
|
||||
| ACCESS_AUTHORIZATION | authentication, identity |
|
||||
| TRANSFER_CONTROL | network, data_protection, encryption |
|
||||
| INPUT_CONTROL | application, data_protection |
|
||||
| ORDER_CONTROL | supply_chain, compliance |
|
||||
| AVAILABILITY | continuity, system |
|
||||
| SEPARATION | network, data_protection |
|
||||
| ENCRYPTION | encryption |
|
||||
| PSEUDONYMIZATION | data_protection, encryption |
|
||||
| RESILIENCE | continuity, system |
|
||||
| RECOVERY | continuity |
|
||||
| REVIEW | compliance, governance, risk |
|
||||
|
||||
### Sync-Algorithmus
|
||||
|
||||
1. CompanyProfile (Branche, Groesse) wird gehasht
|
||||
2. Aenderung erkannt → alte auto-Mappings loeschen
|
||||
3. Pro TOM-Kategorie: Canonical Controls mit passender `category`, `applicable_industries`, `applicable_company_size` und `release_state = 'approved'` suchen
|
||||
4. Neue Mappings inserieren (ON CONFLICT DO NOTHING)
|
||||
5. Sync-State aktualisieren
|
||||
|
||||
---
|
||||
|
||||
## Cross-Modul-Integration
|
||||
|
||||
| Modul | Integration |
|
||||
|-------|------------|
|
||||
| **DSFA** | TOM-Controls als Massnahmen in Datenschutz-Folgenabschaetzung |
|
||||
| **VVT** | Verknuepfung von TOMs mit Verarbeitungstaetigkeiten |
|
||||
| **Loeschfristen** | Loeschkontrollen (TOM-DL) referenzieren Loeschfristen-Policies |
|
||||
| **Control Library** | Canonical Controls als belegende Security-Controls pro TOM-Kategorie |
|
||||
| **Risk Assessment** | RiskProfile steuert Applicability der Controls |
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
Scope["Scope Engine"] -->|Profile| TOM["TOM (Art. 32)"]
|
||||
CLIB["Control Library"] -->|Canonical Controls| TOM
|
||||
TOM --> DSFA["DSFA (Art. 35)"]
|
||||
TOM --> VVT["VVT (Art. 30)"]
|
||||
TOM --> Loeschfristen["Loeschfristen"]
|
||||
Risk["Risk Assessment"] -->|RiskProfile| TOM
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Audit-Faehigkeit
|
||||
|
||||
Das TOM-Modul erfuellt 7 Audit-Kriterien:
|
||||
|
||||
1. **Vollstaendigkeit**: 88 Controls decken alle Art. 32 Anforderungen ab
|
||||
2. **Nachvollziehbarkeit**: Rules Engine dokumentiert Applicability-Gruende
|
||||
3. **Aktualitaet**: Review-Zyklen + Compliance-Check OVERDUE_REVIEW
|
||||
4. **Verantwortlichkeit**: Rollenmatrix im TOM-Dokument
|
||||
5. **Evidence**: Evidence-Verknuepfung + Gap-Analyse
|
||||
6. **Druckfaehigkeit**: Auditfaehiges HTML-Dokument mit 12 Sektionen
|
||||
7. **Wirksamkeitspruefung**: 11 Compliance-Checks + Score
|
||||
|
||||
---
|
||||
|
||||
## Datei-Uebersicht
|
||||
|
||||
| Datei | Beschreibung |
|
||||
|-------|--------------|
|
||||
| `admin-compliance/app/sdk/tom/page.tsx` | Haupt-Seite mit 5 Tabs |
|
||||
| `admin-compliance/components/sdk/tom-dashboard/` | Tab-Komponenten (Overview, Editor, GapExport, Document) |
|
||||
| `admin-compliance/lib/sdk/tom-generator/types.ts` | Alle TypeScript-Typen |
|
||||
| `admin-compliance/lib/sdk/tom-generator/controls/loader.ts` | 88 Embedded Controls |
|
||||
| `admin-compliance/lib/sdk/tom-generator/rules-engine.ts` | Applicability-Auswertung |
|
||||
| `admin-compliance/lib/sdk/tom-generator/context.tsx` | State Management (Reducer) |
|
||||
| `admin-compliance/lib/sdk/tom-generator/sdm-mapping.ts` | SDM-Gewaehrleistungsziel-Mapping |
|
||||
| `admin-compliance/lib/sdk/tom-compliance.ts` | 11 Compliance-Checks |
|
||||
| `admin-compliance/lib/sdk/tom-document.ts` | HTML-Dokument-Generator |
|
||||
| `backend-compliance/compliance/api/tom_mapping_routes.py` | TOM ↔ Canonical Control Mapping API (6 Endpoints) |
|
||||
| `backend-compliance/migrations/068_tom_control_mappings.sql` | DB-Schema fuer Mapping-Bridge |
|
||||
@@ -74,7 +74,10 @@ nav:
|
||||
- Risiken (CP-RSK): services/sdk-modules/risks.md
|
||||
- Analyse-Module (Paket 2): services/sdk-modules/analyse-module.md
|
||||
- Dokumentations-Module (Paket 3+): services/sdk-modules/dokumentations-module.md
|
||||
- VVT (Art. 30 DSGVO): services/sdk-modules/vvt.md
|
||||
- Loeschfristen (Loeschkonzept): services/sdk-modules/loeschfristen.md
|
||||
- DSFA (Art. 35 DSGVO): services/sdk-modules/dsfa.md
|
||||
- TOM (Art. 32 DSGVO): services/sdk-modules/tom.md
|
||||
- Rechtliche Texte (Paket 4): services/sdk-modules/rechtliche-texte.md
|
||||
- DSR (Betroffenenrechte): services/sdk-modules/dsr.md
|
||||
- E-Mail-Templates: services/sdk-modules/email-templates.md
|
||||
|
||||
Reference in New Issue
Block a user