Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
354 lines
10 KiB
TypeScript
354 lines
10 KiB
TypeScript
// =============================================================================
|
|
// Loeschfristen Module - Export & Report Generation
|
|
// JSON, CSV, Markdown-Compliance-Report und Browser-Download
|
|
// =============================================================================
|
|
|
|
import {
|
|
LoeschfristPolicy,
|
|
RETENTION_DRIVER_META,
|
|
DELETION_METHOD_LABELS,
|
|
STATUS_LABELS,
|
|
TRIGGER_LABELS,
|
|
formatRetentionDuration,
|
|
getEffectiveDeletionTrigger,
|
|
} from './loeschfristen-types'
|
|
|
|
import {
|
|
runComplianceCheck,
|
|
ComplianceCheckResult,
|
|
ComplianceIssueSeverity,
|
|
} from './loeschfristen-compliance'
|
|
|
|
// =============================================================================
|
|
// JSON EXPORT
|
|
// =============================================================================
|
|
|
|
interface PolicyExportEnvelope {
|
|
exportDate: string
|
|
version: string
|
|
totalPolicies: number
|
|
policies: LoeschfristPolicy[]
|
|
}
|
|
|
|
/**
|
|
* Exportiert alle Policies als pretty-printed JSON.
|
|
* Enthaelt Metadaten (Exportdatum, Version, Anzahl).
|
|
*/
|
|
export function exportPoliciesAsJSON(policies: LoeschfristPolicy[]): string {
|
|
const exportData: PolicyExportEnvelope = {
|
|
exportDate: new Date().toISOString(),
|
|
version: '1.0',
|
|
totalPolicies: policies.length,
|
|
policies: policies,
|
|
}
|
|
return JSON.stringify(exportData, null, 2)
|
|
}
|
|
|
|
// =============================================================================
|
|
// CSV EXPORT
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Escapes a CSV field value according to RFC 4180.
|
|
* Fields containing commas, double quotes, or newlines are wrapped in quotes.
|
|
* Existing double quotes are doubled.
|
|
*/
|
|
function escapeCSVField(value: string): string {
|
|
if (
|
|
value.includes(',') ||
|
|
value.includes('"') ||
|
|
value.includes('\n') ||
|
|
value.includes('\r') ||
|
|
value.includes(';')
|
|
) {
|
|
return `"${value.replace(/"/g, '""')}"`
|
|
}
|
|
return value
|
|
}
|
|
|
|
/**
|
|
* Formats a date string to German locale format (DD.MM.YYYY).
|
|
* Returns empty string for null/undefined/empty values.
|
|
*/
|
|
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 ''
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Exportiert alle Policies als CSV mit BOM fuer Excel-Kompatibilitaet.
|
|
* Trennzeichen ist Semikolon (;) fuer deutschsprachige Excel-Versionen.
|
|
*/
|
|
export function exportPoliciesAsCSV(policies: LoeschfristPolicy[]): string {
|
|
const BOM = '\uFEFF'
|
|
const SEPARATOR = ';'
|
|
|
|
const headers = [
|
|
'LF-Nr.',
|
|
'Datenobjekt',
|
|
'Beschreibung',
|
|
'Loeschtrigger',
|
|
'Aufbewahrungstreiber',
|
|
'Frist',
|
|
'Startereignis',
|
|
'Loeschmethode',
|
|
'Verantwortlich',
|
|
'Status',
|
|
'Legal Hold aktiv',
|
|
'Letzte Pruefung',
|
|
'Naechste Pruefung',
|
|
]
|
|
|
|
const rows: string[] = []
|
|
|
|
// Header row
|
|
rows.push(headers.map(escapeCSVField).join(SEPARATOR))
|
|
|
|
// Data rows
|
|
for (const policy of policies) {
|
|
const effectiveTrigger = getEffectiveDeletionTrigger(policy)
|
|
const triggerLabel = TRIGGER_LABELS[effectiveTrigger]
|
|
|
|
const driverLabel = policy.retentionDriver
|
|
? RETENTION_DRIVER_META[policy.retentionDriver].label
|
|
: ''
|
|
|
|
const durationLabel = formatRetentionDuration(
|
|
policy.retentionDuration,
|
|
policy.retentionUnit
|
|
)
|
|
|
|
const methodLabel = DELETION_METHOD_LABELS[policy.deletionMethod]
|
|
const statusLabel = STATUS_LABELS[policy.status]
|
|
|
|
// Combine responsiblePerson and responsibleRole
|
|
const responsible = [policy.responsiblePerson, policy.responsibleRole]
|
|
.filter((s) => s.trim())
|
|
.join(' / ')
|
|
|
|
const legalHoldActive = policy.hasActiveLegalHold ? 'Ja' : 'Nein'
|
|
|
|
const row = [
|
|
policy.policyId,
|
|
policy.dataObjectName,
|
|
policy.description,
|
|
triggerLabel,
|
|
driverLabel,
|
|
durationLabel,
|
|
policy.startEvent,
|
|
methodLabel,
|
|
responsible || '-',
|
|
statusLabel,
|
|
legalHoldActive,
|
|
formatDateDE(policy.lastReviewDate),
|
|
formatDateDE(policy.nextReviewDate),
|
|
]
|
|
|
|
rows.push(row.map(escapeCSVField).join(SEPARATOR))
|
|
}
|
|
|
|
return BOM + rows.join('\r\n')
|
|
}
|
|
|
|
// =============================================================================
|
|
// COMPLIANCE SUMMARY (MARKDOWN)
|
|
// =============================================================================
|
|
|
|
const SEVERITY_LABELS: Record<ComplianceIssueSeverity, string> = {
|
|
CRITICAL: 'Kritisch',
|
|
HIGH: 'Hoch',
|
|
MEDIUM: 'Mittel',
|
|
LOW: 'Niedrig',
|
|
}
|
|
|
|
const SEVERITY_EMOJI: Record<ComplianceIssueSeverity, string> = {
|
|
CRITICAL: '[!!!]',
|
|
HIGH: '[!!]',
|
|
MEDIUM: '[!]',
|
|
LOW: '[i]',
|
|
}
|
|
|
|
/**
|
|
* Returns a textual rating based on the compliance score.
|
|
*/
|
|
function getScoreRating(score: number): string {
|
|
if (score >= 90) return 'Ausgezeichnet'
|
|
if (score >= 75) return 'Gut'
|
|
if (score >= 50) return 'Verbesserungswuerdig'
|
|
if (score >= 25) return 'Mangelhaft'
|
|
return 'Kritisch'
|
|
}
|
|
|
|
/**
|
|
* Generiert einen Markdown-formatierten Compliance-Bericht.
|
|
* Enthaelt: Uebersicht, Score, Issue-Liste, Empfehlungen.
|
|
*/
|
|
export function generateComplianceSummary(
|
|
policies: LoeschfristPolicy[],
|
|
vvtDataCategories?: string[]
|
|
): string {
|
|
const result: ComplianceCheckResult = runComplianceCheck(policies, vvtDataCategories)
|
|
const now = new Date()
|
|
|
|
const lines: string[] = []
|
|
|
|
// Header
|
|
lines.push('# Compliance-Bericht: Loeschfristen')
|
|
lines.push('')
|
|
lines.push(
|
|
`**Erstellt am:** ${now.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })} um ${now.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })} Uhr`
|
|
)
|
|
lines.push('')
|
|
|
|
// Overview
|
|
lines.push('## Uebersicht')
|
|
lines.push('')
|
|
lines.push(`| Kennzahl | Wert |`)
|
|
lines.push(`|----------|------|`)
|
|
lines.push(`| Gepruefte Policies | ${result.stats.total} |`)
|
|
lines.push(`| Bestanden | ${result.stats.passed} |`)
|
|
lines.push(`| Beanstandungen | ${result.stats.failed} |`)
|
|
lines.push(`| Compliance-Score | **${result.score}/100** (${getScoreRating(result.score)}) |`)
|
|
lines.push('')
|
|
|
|
// Severity breakdown
|
|
lines.push('## Befunde nach Schweregrad')
|
|
lines.push('')
|
|
lines.push('| Schweregrad | Anzahl |')
|
|
lines.push('|-------------|--------|')
|
|
|
|
const severityOrder: ComplianceIssueSeverity[] = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']
|
|
for (const severity of severityOrder) {
|
|
const count = result.stats.bySeverity[severity]
|
|
lines.push(`| ${SEVERITY_LABELS[severity]} | ${count} |`)
|
|
}
|
|
lines.push('')
|
|
|
|
// Status distribution of policies
|
|
const statusCounts: Record<string, number> = {}
|
|
for (const policy of policies) {
|
|
const label = STATUS_LABELS[policy.status]
|
|
statusCounts[label] = (statusCounts[label] || 0) + 1
|
|
}
|
|
|
|
lines.push('## Policy-Status-Verteilung')
|
|
lines.push('')
|
|
lines.push('| Status | Anzahl |')
|
|
lines.push('|--------|--------|')
|
|
for (const [label, count] of Object.entries(statusCounts)) {
|
|
lines.push(`| ${label} | ${count} |`)
|
|
}
|
|
lines.push('')
|
|
|
|
// Issues list
|
|
if (result.issues.length === 0) {
|
|
lines.push('## Befunde')
|
|
lines.push('')
|
|
lines.push('Keine Beanstandungen gefunden. Alle Policies sind konform.')
|
|
lines.push('')
|
|
} else {
|
|
lines.push('## Befunde')
|
|
lines.push('')
|
|
|
|
// Group issues by severity
|
|
for (const severity of severityOrder) {
|
|
const issuesForSeverity = result.issues.filter((i) => i.severity === severity)
|
|
if (issuesForSeverity.length === 0) continue
|
|
|
|
lines.push(`### ${SEVERITY_LABELS[severity]} ${SEVERITY_EMOJI[severity]}`)
|
|
lines.push('')
|
|
|
|
for (const issue of issuesForSeverity) {
|
|
const policyRef =
|
|
issue.policyId !== '-' ? ` (${issue.policyId})` : ''
|
|
lines.push(`**${issue.title}**${policyRef}`)
|
|
lines.push('')
|
|
lines.push(`> ${issue.description}`)
|
|
lines.push('')
|
|
lines.push(`Empfehlung: ${issue.recommendation}`)
|
|
lines.push('')
|
|
lines.push('---')
|
|
lines.push('')
|
|
}
|
|
}
|
|
}
|
|
|
|
// Recommendations summary
|
|
lines.push('## Zusammenfassung der Empfehlungen')
|
|
lines.push('')
|
|
|
|
if (result.stats.bySeverity.CRITICAL > 0) {
|
|
lines.push(
|
|
`1. **Sofortmassnahmen erforderlich:** ${result.stats.bySeverity.CRITICAL} kritische(r) Befund(e) muessen umgehend behoben werden (Legal Hold-Konflikte).`
|
|
)
|
|
}
|
|
if (result.stats.bySeverity.HIGH > 0) {
|
|
lines.push(
|
|
`${result.stats.bySeverity.CRITICAL > 0 ? '2' : '1'}. **Hohe Prioritaet:** ${result.stats.bySeverity.HIGH} Befund(e) mit hoher Prioritaet (fehlende Trigger/Rechtsgrundlagen) sollten zeitnah bearbeitet werden.`
|
|
)
|
|
}
|
|
if (result.stats.bySeverity.MEDIUM > 0) {
|
|
lines.push(
|
|
`- **Mittlere Prioritaet:** ${result.stats.bySeverity.MEDIUM} Befund(e) betreffen ueberfaellige Pruefungen, fehlende Verantwortlichkeiten oder nicht abgedeckte Datenkategorien.`
|
|
)
|
|
}
|
|
if (result.stats.bySeverity.LOW > 0) {
|
|
lines.push(
|
|
`- **Niedrige Prioritaet:** ${result.stats.bySeverity.LOW} Befund(e) betreffen veraltete Entwuerfe, die finalisiert oder archiviert werden sollten.`
|
|
)
|
|
}
|
|
if (result.issues.length === 0) {
|
|
lines.push(
|
|
'Alle Policies sind konform. Stellen Sie sicher, dass die naechsten Pruefungstermine eingehalten werden.'
|
|
)
|
|
}
|
|
lines.push('')
|
|
|
|
// Footer
|
|
lines.push('---')
|
|
lines.push('')
|
|
lines.push(
|
|
'*Dieser Bericht wurde automatisch generiert und ersetzt keine rechtliche Beratung. Die Verantwortung fuer die DSGVO-Konformitaet liegt beim Verantwortlichen (Art. 4 Nr. 7 DSGVO).*'
|
|
)
|
|
|
|
return lines.join('\n')
|
|
}
|
|
|
|
// =============================================================================
|
|
// BROWSER DOWNLOAD UTILITY
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Loest einen Datei-Download im Browser aus.
|
|
* Erstellt ein temporaeres Blob-URL und simuliert einen Link-Klick.
|
|
*
|
|
* @param content - Der Dateiinhalt als String
|
|
* @param filename - Der gewuenschte Dateiname (z.B. "loeschfristen-export.json")
|
|
* @param mimeType - Der MIME-Typ (z.B. "application/json", "text/csv;charset=utf-8")
|
|
*/
|
|
export function downloadFile(
|
|
content: string,
|
|
filename: string,
|
|
mimeType: string
|
|
): void {
|
|
const blob = new Blob([content], { type: mimeType })
|
|
const url = URL.createObjectURL(blob)
|
|
const link = document.createElement('a')
|
|
link.href = url
|
|
link.download = filename
|
|
document.body.appendChild(link)
|
|
link.click()
|
|
document.body.removeChild(link)
|
|
URL.revokeObjectURL(url)
|
|
}
|