obligations-document, tom-document, loeschfristen-document, compliance-scope-triggers, sdk-flow/flow-data, processing-activities, loeschfristen-baseline-catalog, catalog-registry, dsfa mitigation-library + risk-catalog, vvt-baseline-catalog, vendor contract-review checklists + findings, demo-data, tom-compliance. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
604 lines
26 KiB
TypeScript
604 lines
26 KiB
TypeScript
// =============================================================================
|
|
// Loeschfristen Document — HTML Document Builder
|
|
// =============================================================================
|
|
|
|
import type {
|
|
LoeschfristPolicy,
|
|
} from '../loeschfristen-types'
|
|
import {
|
|
RETENTION_DRIVER_META,
|
|
DELETION_METHOD_LABELS,
|
|
STATUS_LABELS,
|
|
TRIGGER_LABELS,
|
|
REVIEW_INTERVAL_LABELS,
|
|
formatRetentionDuration,
|
|
getEffectiveDeletionTrigger,
|
|
getActiveLegalHolds,
|
|
} from '../loeschfristen-types'
|
|
import type { ComplianceCheckResult, ComplianceIssueSeverity } from '../loeschfristen-compliance'
|
|
import type { LoeschkonzeptOrgHeader, LoeschkonzeptRevision } from './types-defaults'
|
|
import { SEVERITY_LABELS_DE, SEVERITY_COLORS } from './types-defaults'
|
|
import { escHtml, formatDateDE } from './helpers'
|
|
|
|
type VVTActivity = { id: string; vvt_id?: string; vvtId?: string; name?: string; activity_name?: string }
|
|
|
|
export function buildLoeschkonzeptHtml(
|
|
policies: LoeschfristPolicy[],
|
|
orgHeader: LoeschkonzeptOrgHeader,
|
|
vvtActivities: VVTActivity[],
|
|
complianceResult: ComplianceCheckResult | null,
|
|
revisions: LoeschkonzeptRevision[]
|
|
): string {
|
|
const today = new Date().toLocaleDateString('de-DE', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric',
|
|
})
|
|
|
|
const activePolicies = policies.filter(p => p.status !== 'ARCHIVED')
|
|
const orgName = orgHeader.organizationName || 'Organisation'
|
|
|
|
// Collect unique storage locations
|
|
const allStorageLocations = new Set<string>()
|
|
for (const p of activePolicies) {
|
|
for (const loc of p.storageLocations) {
|
|
allStorageLocations.add(loc.name || loc.type)
|
|
}
|
|
}
|
|
|
|
// Collect unique responsible roles
|
|
const roleMap = new Map<string, string[]>()
|
|
for (const p of activePolicies) {
|
|
const role = p.responsibleRole || p.responsiblePerson || 'Nicht zugewiesen'
|
|
if (!roleMap.has(role)) roleMap.set(role, [])
|
|
roleMap.get(role)!.push(p.dataObjectName || p.policyId)
|
|
}
|
|
|
|
// Collect active legal holds
|
|
const allActiveLegalHolds: Array<{ policy: string; hold: LoeschfristPolicy['legalHolds'][0] }> = []
|
|
for (const p of activePolicies) {
|
|
for (const h of getActiveLegalHolds(p)) {
|
|
allActiveLegalHolds.push({ policy: p.dataObjectName || p.policyId, hold: h })
|
|
}
|
|
}
|
|
|
|
// Build VVT cross-reference data
|
|
const vvtRefs: Array<{ policyName: string; policyId: string; vvtId: string; vvtName: string }> = []
|
|
for (const p of activePolicies) {
|
|
for (const linkedId of p.linkedVVTActivityIds) {
|
|
const activity = vvtActivities.find(a => a.id === linkedId)
|
|
if (activity) {
|
|
vvtRefs.push({
|
|
policyName: p.dataObjectName || p.policyId,
|
|
policyId: p.policyId,
|
|
vvtId: activity.vvt_id || activity.vvtId || linkedId.substring(0, 8),
|
|
vvtName: activity.activity_name || activity.name || 'Unbenannte Verarbeitungstaetigkeit',
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build vendor cross-reference data
|
|
const vendorRefs: Array<{ policyName: string; policyId: string; vendorId: string; duration: string }> = []
|
|
for (const p of activePolicies) {
|
|
if (p.linkedVendorIds && p.linkedVendorIds.length > 0) {
|
|
for (const vendorId of p.linkedVendorIds) {
|
|
vendorRefs.push({
|
|
policyName: p.dataObjectName || p.policyId,
|
|
policyId: p.policyId,
|
|
vendorId,
|
|
duration: formatRetentionDuration(p.retentionDuration, p.retentionUnit),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
let html = buildDocumentHeader(orgName)
|
|
html += buildCoverAndTOC(orgHeader, orgName, today)
|
|
html += buildSections1to5(orgName, orgHeader, activePolicies, allStorageLocations)
|
|
html += buildSections6to9(activePolicies, vvtRefs, vendorRefs, allActiveLegalHolds, roleMap)
|
|
html += buildSections10to12(orgHeader, complianceResult, revisions, today)
|
|
html += `
|
|
<div class="page-footer">
|
|
<span>Loeschkonzept — ${escHtml(orgName)}</span>
|
|
<span>Stand: ${today} | Version ${escHtml(orgHeader.loeschkonzeptVersion)}</span>
|
|
</div>
|
|
|
|
</body>
|
|
</html>`
|
|
|
|
return html
|
|
}
|
|
|
|
function buildDocumentHeader(orgName: string): string {
|
|
return `<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Loeschkonzept — ${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 { 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 { 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; }
|
|
.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; }
|
|
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; }
|
|
.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; }
|
|
.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; }
|
|
.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-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; }
|
|
.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; }
|
|
@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>
|
|
`
|
|
}
|
|
|
|
function buildCoverAndTOC(
|
|
orgHeader: LoeschkonzeptOrgHeader,
|
|
orgName: string,
|
|
today: string
|
|
): string {
|
|
const sections = [
|
|
'Ziel und Zweck', 'Geltungsbereich', 'Grundprinzipien der Datenspeicherung',
|
|
'Loeschregeln-Uebersicht', 'Detaillierte Loeschregeln', 'VVT-Verknuepfung',
|
|
'Auftragsverarbeiter mit Loeschpflichten', 'Legal Hold Verfahren',
|
|
'Verantwortlichkeiten', 'Pruef- und Revisionszyklus', 'Compliance-Status', 'Aenderungshistorie',
|
|
]
|
|
|
|
return `
|
|
<div class="cover">
|
|
<h1>Loeschkonzept</h1>
|
|
<div class="subtitle">gemaess Art. 5 Abs. 1 lit. e, Art. 17, Art. 30 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">Datenschutzbeauftragter:</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.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.loeschkonzeptVersion)} | Stand: ${today}<br/>
|
|
Letzte Pruefung: ${formatDateDE(orgHeader.lastReviewDate)} | Naechste Pruefung: ${formatDateDE(orgHeader.nextReviewDate)}<br/>
|
|
Pruefintervall: ${escHtml(orgHeader.reviewInterval)}
|
|
</div>
|
|
</div>
|
|
<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>
|
|
`
|
|
}
|
|
|
|
function buildSections1to5(
|
|
orgName: string,
|
|
orgHeader: LoeschkonzeptOrgHeader,
|
|
activePolicies: LoeschfristPolicy[],
|
|
allStorageLocations: Set<string>
|
|
): string {
|
|
const storageListHtml = allStorageLocations.size > 0
|
|
? Array.from(allStorageLocations).map(s => `<li>${escHtml(s)}</li>`).join('')
|
|
: '<li><em>Keine Speicherorte dokumentiert</em></li>'
|
|
|
|
return `
|
|
<div class="section">
|
|
<div class="section-header">1. Ziel und Zweck</div>
|
|
<div class="section-body">
|
|
<p>Dieses Loeschkonzept definiert die systematischen Regeln und Verfahren fuer die Loeschung
|
|
personenbezogener Daten bei <strong>${escHtml(orgName)}</strong>. Es dient der Umsetzung
|
|
folgender DSGVO-Anforderungen:</p>
|
|
<table>
|
|
<tr><th>Rechtsgrundlage</th><th>Inhalt</th></tr>
|
|
<tr><td><strong>Art. 5 Abs. 1 lit. e DSGVO</strong></td><td>Grundsatz der Speicherbegrenzung — personenbezogene Daten duerfen nur so lange gespeichert werden, wie es fuer die Zwecke der Verarbeitung erforderlich ist.</td></tr>
|
|
<tr><td><strong>Art. 17 DSGVO</strong></td><td>Recht auf Loeschung („Recht auf Vergessenwerden“) — Betroffene haben das Recht, die Loeschung ihrer Daten zu verlangen.</td></tr>
|
|
<tr><td><strong>Art. 30 DSGVO</strong></td><td>Verzeichnis von Verarbeitungstaetigkeiten — vorgesehene Fristen fuer die Loeschung der verschiedenen Datenkategorien muessen dokumentiert werden.</td></tr>
|
|
</table>
|
|
<p>Das Loeschkonzept ist fester Bestandteil des Datenschutz-Managementsystems und wird
|
|
regelmaessig ueberprueft und aktualisiert.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-header">2. Geltungsbereich</div>
|
|
<div class="section-body">
|
|
<p>Dieses Loeschkonzept gilt fuer alle personenbezogenen Daten, die von <strong>${escHtml(orgName)}</strong>
|
|
verarbeitet werden. Es umfasst <strong>${activePolicies.length}</strong> Loeschregeln fuer folgende Systeme und Speicherorte:</p>
|
|
<ul style="margin: 8px 0 8px 24px;">${storageListHtml}</ul>
|
|
<p>Saemtliche Verarbeitungstaetigkeiten, die im Verzeichnis von Verarbeitungstaetigkeiten (VVT)
|
|
erfasst sind, werden durch dieses Loeschkonzept abgedeckt.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-header">3. Grundprinzipien der Datenspeicherung</div>
|
|
<div class="section-body">
|
|
<div class="principle"><strong>Speicherbegrenzung:</strong> Personenbezogene Daten werden nur so lange gespeichert, wie es fuer den jeweiligen Verarbeitungszweck erforderlich ist (Art. 5 Abs. 1 lit. e DSGVO).</div>
|
|
<div class="principle"><strong>3-Level-Loeschlogik:</strong> Die Loeschung folgt einer dreistufigen Priorisierung: (1) Zweckende, (2) gesetzliche Aufbewahrungspflichten, (3) Legal Hold — jeweils mit der laengsten Frist als massgeblich.</div>
|
|
<div class="principle"><strong>Dokumentationspflicht:</strong> Jede Loeschregel ist dokumentiert mit Rechtsgrundlage, Frist, Loeschmethode und Verantwortlichkeit.</div>
|
|
<div class="principle"><strong>Regelmaessige Ueberpruefung:</strong> Alle Loeschregeln werden im definierten Intervall ueberprueft und bei Bedarf angepasst.</div>
|
|
<div class="principle"><strong>Datenschutz durch Technikgestaltung:</strong> Loeschmechanismen werden moeglichst automatisiert, um menschliche Fehler zu minimieren (Art. 25 DSGVO).</div>
|
|
</div>
|
|
</div>
|
|
|
|
${buildSection4Overview(activePolicies)}
|
|
${buildSection5Detail(activePolicies)}
|
|
`
|
|
}
|
|
|
|
function buildSection4Overview(activePolicies: LoeschfristPolicy[]): string {
|
|
let html = `
|
|
<div class="section page-break">
|
|
<div class="section-header">4. Loeschregeln-Uebersicht</div>
|
|
<div class="section-body">
|
|
<p>Die folgende Tabelle zeigt eine Uebersicht aller ${activePolicies.length} aktiven Loeschregeln:</p>
|
|
<table>
|
|
<tr>
|
|
<th>LF-Nr.</th>
|
|
<th>Datenobjekt</th>
|
|
<th>Loeschtrigger</th>
|
|
<th>Aufbewahrungsfrist</th>
|
|
<th>Loeschmethode</th>
|
|
<th>Status</th>
|
|
</tr>
|
|
`
|
|
for (const p of activePolicies) {
|
|
const trigger = TRIGGER_LABELS[getEffectiveDeletionTrigger(p)]
|
|
const duration = formatRetentionDuration(p.retentionDuration, p.retentionUnit)
|
|
const method = DELETION_METHOD_LABELS[p.deletionMethod]
|
|
const statusLabel = STATUS_LABELS[p.status]
|
|
const statusClass = p.status === 'ACTIVE' ? 'badge-active' : p.status === 'REVIEW_NEEDED' ? 'badge-review' : 'badge-draft'
|
|
|
|
html += ` <tr>
|
|
<td>${escHtml(p.policyId)}</td>
|
|
<td>${escHtml(p.dataObjectName)}</td>
|
|
<td>${escHtml(trigger)}</td>
|
|
<td>${escHtml(duration)}</td>
|
|
<td>${escHtml(method)}</td>
|
|
<td><span class="badge ${statusClass}">${escHtml(statusLabel)}</span></td>
|
|
</tr>
|
|
`
|
|
}
|
|
|
|
html += ` </table>
|
|
</div>
|
|
</div>
|
|
`
|
|
return html
|
|
}
|
|
|
|
function buildSection5Detail(activePolicies: LoeschfristPolicy[]): string {
|
|
let html = `
|
|
<div class="section">
|
|
<div class="section-header">5. Detaillierte Loeschregeln</div>
|
|
<div class="section-body">
|
|
`
|
|
for (const p of activePolicies) {
|
|
const trigger = TRIGGER_LABELS[getEffectiveDeletionTrigger(p)]
|
|
const duration = formatRetentionDuration(p.retentionDuration, p.retentionUnit)
|
|
const method = DELETION_METHOD_LABELS[p.deletionMethod]
|
|
const statusLabel = STATUS_LABELS[p.status]
|
|
const driverLabel = p.retentionDriver ? RETENTION_DRIVER_META[p.retentionDriver]?.label || p.retentionDriver : '-'
|
|
const driverStatute = p.retentionDriver ? RETENTION_DRIVER_META[p.retentionDriver]?.statute || '' : ''
|
|
const locations = p.storageLocations.map(l => l.name || l.type).join(', ') || '-'
|
|
const responsible = [p.responsiblePerson, p.responsibleRole].filter(s => s.trim()).join(' / ') || '-'
|
|
const activeHolds = getActiveLegalHolds(p)
|
|
|
|
html += `
|
|
<div class="policy-detail">
|
|
<div class="policy-detail-header">
|
|
<span>${escHtml(p.policyId)} — ${escHtml(p.dataObjectName)}</span>
|
|
<span class="badge ${p.status === 'ACTIVE' ? 'badge-active' : 'badge-draft'}">${escHtml(statusLabel)}</span>
|
|
</div>
|
|
<div class="policy-detail-body">
|
|
<table>
|
|
<tr><th>Beschreibung</th><td>${escHtml(p.description || '-')}</td></tr>
|
|
<tr><th>Betroffenengruppen</th><td>${escHtml(p.affectedGroups.join(', ') || '-')}</td></tr>
|
|
<tr><th>Datenkategorien</th><td>${escHtml(p.dataCategories.join(', ') || '-')}</td></tr>
|
|
<tr><th>Verarbeitungszweck</th><td>${escHtml(p.primaryPurpose || '-')}</td></tr>
|
|
<tr><th>Loeschtrigger</th><td>${escHtml(trigger)}</td></tr>
|
|
<tr><th>Aufbewahrungstreiber</th><td>${escHtml(driverLabel)}${driverStatute ? ` (${escHtml(driverStatute)})` : ''}</td></tr>
|
|
<tr><th>Aufbewahrungsfrist</th><td>${escHtml(duration)}</td></tr>
|
|
<tr><th>Startereignis</th><td>${escHtml(p.startEvent || '-')}</td></tr>
|
|
<tr><th>Loeschmethode</th><td>${escHtml(method)}</td></tr>
|
|
<tr><th>Loeschmethode (Detail)</th><td>${escHtml(p.deletionMethodDetail || '-')}</td></tr>
|
|
<tr><th>Speicherorte</th><td>${escHtml(locations)}</td></tr>
|
|
<tr><th>Verantwortlich</th><td>${escHtml(responsible)}</td></tr>
|
|
<tr><th>Pruefintervall</th><td>${escHtml(REVIEW_INTERVAL_LABELS[p.reviewInterval] || p.reviewInterval)}</td></tr>
|
|
${activeHolds.length > 0 ? `<tr><th>Aktive Legal Holds</th><td>${activeHolds.map(h => `${escHtml(h.reason)} (seit ${formatDateDE(h.startDate)})`).join('<br/>')}</td></tr>` : ''}
|
|
</table>
|
|
</div>
|
|
</div>
|
|
`
|
|
}
|
|
|
|
html += ` </div>
|
|
</div>
|
|
`
|
|
return html
|
|
}
|
|
|
|
function buildSections6to9(
|
|
activePolicies: LoeschfristPolicy[],
|
|
vvtRefs: Array<{ policyName: string; policyId: string; vvtId: string; vvtName: string }>,
|
|
vendorRefs: Array<{ policyName: string; policyId: string; vendorId: string; duration: string }>,
|
|
allActiveLegalHolds: Array<{ policy: string; hold: LoeschfristPolicy['legalHolds'][0] }>,
|
|
roleMap: Map<string, string[]>
|
|
): string {
|
|
let html = `
|
|
<div class="section page-break">
|
|
<div class="section-header">6. VVT-Verknuepfung</div>
|
|
<div class="section-body">
|
|
<p>Die folgende Tabelle zeigt die Verknuepfung zwischen Loeschregeln und Verarbeitungstaetigkeiten im VVT (Art. 30 DSGVO):</p>
|
|
`
|
|
if (vvtRefs.length > 0) {
|
|
html += ` <table>
|
|
<tr><th>Loeschregel</th><th>LF-Nr.</th><th>VVT-Nr.</th><th>Verarbeitungstaetigkeit</th></tr>
|
|
`
|
|
for (const ref of vvtRefs) {
|
|
html += ` <tr>
|
|
<td>${escHtml(ref.policyName)}</td>
|
|
<td>${escHtml(ref.policyId)}</td>
|
|
<td>${escHtml(ref.vvtId)}</td>
|
|
<td>${escHtml(ref.vvtName)}</td>
|
|
</tr>
|
|
`
|
|
}
|
|
html += ` </table>
|
|
`
|
|
} else {
|
|
html += ` <p><em>Noch keine VVT-Verknuepfungen dokumentiert.</em></p>
|
|
`
|
|
}
|
|
html += ` </div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-header">7. Auftragsverarbeiter mit Loeschpflichten</div>
|
|
<div class="section-body">
|
|
<p>Die folgende Tabelle zeigt Loeschregeln, die mit Auftragsverarbeitern verknuepft sind (Art. 28 DSGVO).</p>
|
|
`
|
|
if (vendorRefs.length > 0) {
|
|
html += ` <table>
|
|
<tr><th>Loeschregel</th><th>LF-Nr.</th><th>Auftragsverarbeiter (ID)</th><th>Aufbewahrungsfrist</th></tr>
|
|
`
|
|
for (const ref of vendorRefs) {
|
|
html += ` <tr>
|
|
<td>${escHtml(ref.policyName)}</td>
|
|
<td>${escHtml(ref.policyId)}</td>
|
|
<td>${escHtml(ref.vendorId)}</td>
|
|
<td>${escHtml(ref.duration)}</td>
|
|
</tr>
|
|
`
|
|
}
|
|
html += ` </table>
|
|
`
|
|
} else {
|
|
html += ` <p><em>Noch keine Auftragsverarbeiter mit Loeschregeln verknuepft.</em></p>
|
|
`
|
|
}
|
|
html += ` </div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-header">8. Legal Hold Verfahren</div>
|
|
<div class="section-body">
|
|
<p>Ein Legal Hold setzt die regulaere Loeschung aus. Betroffene Daten duerfen trotz abgelaufener Loeschfrist nicht geloescht werden, bis der Legal Hold aufgehoben wird.</p>
|
|
<p><strong>Verfahrensschritte:</strong></p>
|
|
<ol style="margin: 8px 0 8px 24px;">
|
|
<li>Rechtsabteilung/DSB identifiziert betroffene Datenkategorien</li>
|
|
<li>Legal Hold wird im System aktiviert (Status: Aktiv)</li>
|
|
<li>Automatische Loeschung wird fuer betroffene Policies ausgesetzt</li>
|
|
<li>Regelmaessige Pruefung, ob der Legal Hold noch erforderlich ist</li>
|
|
<li>Nach Aufhebung: Regulaere Loeschfristen greifen wieder</li>
|
|
</ol>
|
|
`
|
|
if (allActiveLegalHolds.length > 0) {
|
|
html += ` <p><strong>Aktuell aktive Legal Holds (${allActiveLegalHolds.length}):</strong></p>
|
|
<table>
|
|
<tr><th>Datenobjekt</th><th>Grund</th><th>Rechtsgrundlage</th><th>Seit</th><th>Voraussichtlich bis</th></tr>
|
|
`
|
|
for (const { policy, hold } of allActiveLegalHolds) {
|
|
html += ` <tr>
|
|
<td>${escHtml(policy)}</td>
|
|
<td>${escHtml(hold.reason)}</td>
|
|
<td>${escHtml(hold.legalBasis)}</td>
|
|
<td>${formatDateDE(hold.startDate)}</td>
|
|
<td>${hold.expectedEndDate ? formatDateDE(hold.expectedEndDate) : 'Unbefristet'}</td>
|
|
</tr>
|
|
`
|
|
}
|
|
html += ` </table>
|
|
`
|
|
} else {
|
|
html += ` <p><em>Derzeit sind keine aktiven Legal Holds vorhanden.</em></p>
|
|
`
|
|
}
|
|
html += ` </div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-header">9. Verantwortlichkeiten</div>
|
|
<div class="section-body">
|
|
<p>Die folgende Rollenmatrix zeigt, welche Organisationseinheiten fuer welche Datenobjekte die Loeschverantwortung tragen:</p>
|
|
<table>
|
|
<tr><th>Rolle / Verantwortlich</th><th>Datenobjekte</th><th>Anzahl</th></tr>
|
|
`
|
|
for (const [role, objects] of roleMap.entries()) {
|
|
html += ` <tr>
|
|
<td>${escHtml(role)}</td>
|
|
<td>${objects.map(o => escHtml(o)).join(', ')}</td>
|
|
<td>${objects.length}</td>
|
|
</tr>
|
|
`
|
|
}
|
|
html += ` </table>
|
|
</div>
|
|
</div>
|
|
`
|
|
return html
|
|
}
|
|
|
|
function buildSections10to12(
|
|
orgHeader: LoeschkonzeptOrgHeader,
|
|
complianceResult: ComplianceCheckResult | null,
|
|
revisions: LoeschkonzeptRevision[],
|
|
today: string
|
|
): string {
|
|
let html = `
|
|
<div class="section">
|
|
<div class="section-header">10. 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.loeschkonzeptVersion)}</td></tr>
|
|
</table>
|
|
<p style="margin-top: 8px;">Bei jeder Pruefung wird das Loeschkonzept auf folgende Punkte ueberprueft:</p>
|
|
<ul style="margin: 8px 0 8px 24px;">
|
|
<li>Vollstaendigkeit aller Loeschregeln (neue Verarbeitungen erfasst?)</li>
|
|
<li>Aktualitaet der gesetzlichen Aufbewahrungsfristen</li>
|
|
<li>Wirksamkeit der technischen Loeschmechanismen</li>
|
|
<li>Einhaltung der definierten Loeschfristen</li>
|
|
<li>Angemessenheit der Verantwortlichkeiten</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
`
|
|
|
|
html += buildSection11Compliance(complianceResult)
|
|
html += buildSection12History(revisions, orgHeader, today)
|
|
return html
|
|
}
|
|
|
|
function buildSection11Compliance(complianceResult: ComplianceCheckResult | null): string {
|
|
let html = `
|
|
<div class="section page-break">
|
|
<div class="section-header">11. 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 Policies</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: ComplianceIssueSeverity[] = ['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 Policies sind konform.</em></p>
|
|
`
|
|
}
|
|
} else {
|
|
html += ` <p><em>Compliance-Check wurde noch nicht ausgefuehrt.</em></p>
|
|
`
|
|
}
|
|
html += ` </div>
|
|
</div>
|
|
`
|
|
return html
|
|
}
|
|
|
|
function buildSection12History(
|
|
revisions: LoeschkonzeptRevision[],
|
|
orgHeader: LoeschkonzeptOrgHeader,
|
|
today: string
|
|
): string {
|
|
let html = `
|
|
<div class="section">
|
|
<div class="section-header">12. 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.loeschkonzeptVersion)}</td>
|
|
<td>${today}</td>
|
|
<td>${escHtml(orgHeader.dpoName || orgHeader.responsiblePerson || '-')}</td>
|
|
<td>Erstversion des Loeschkonzepts</td>
|
|
</tr>
|
|
`
|
|
}
|
|
html += ` </table>
|
|
</div>
|
|
</div>
|
|
`
|
|
return html
|
|
}
|