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'],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user