import { AuditAction, AuditSensitivity, Prisma } from '@prisma/client'; import crypto from 'crypto'; import { encrypt, decrypt } from '../utils/encryption.js'; import prisma from '../lib/prisma.js'; /** * Vereinfachte Audit-Log-Funktion für gezielte Änderungsprotokolle. * Wird direkt in Controllern aufgerufen mit aussagekräftigen Details. */ export async function logChange(opts: { req: any; // Express Request (für userId, email, IP) action: AuditAction; resourceType: string; resourceId?: string; label: string; // Menschenlesbares Label z.B. "Vollmacht für Stefan Hacker widerrufen" details?: Record; // Zusätzliche Details z.B. { vorher: 'erteilt', nachher: 'widerrufen' } customerId?: number; }) { try { const user = opts.req?.user; await createAuditLog({ userId: user?.userId, userEmail: user?.email || 'system', userRole: user?.isCustomerPortal ? 'Kundenportal' : 'Mitarbeiter', customerId: user?.customerId, isCustomerPortal: user?.isCustomerPortal, action: opts.action, resourceType: opts.resourceType, resourceId: opts.resourceId, resourceLabel: opts.label, endpoint: opts.req?.path || '', httpMethod: opts.req?.method || '', ipAddress: opts.req?.socket?.remoteAddress || opts.req?.headers?.['x-forwarded-for'] || 'unknown', dataSubjectId: opts.customerId, changesAfter: opts.details, }); } catch (error) { console.error('[logChange] Fehler:', error); } } export interface CreateAuditLogData { userId?: number; userEmail: string; userRole?: string; customerId?: number; isCustomerPortal?: boolean; action: AuditAction; sensitivity?: AuditSensitivity; resourceType: string; resourceId?: string; resourceLabel?: string; endpoint: string; httpMethod: string; ipAddress: string; userAgent?: string; changesBefore?: Record; changesAfter?: Record; dataSubjectId?: number; legalBasis?: string; success?: boolean; errorMessage?: string; durationMs?: number; } export interface AuditLogSearchParams { userId?: number; customerId?: number; dataSubjectId?: number; action?: AuditAction; sensitivity?: AuditSensitivity; resourceType?: string; resourceId?: string; startDate?: Date; endDate?: Date; success?: boolean; search?: string; page?: number; limit?: number; } /** * Generiert einen SHA-256 Hash für einen Audit-Log-Eintrag */ function generateHash(data: { userEmail: string; action: AuditAction; resourceType: string; resourceId?: string | null; endpoint: string; createdAt: Date; previousHash?: string | null; }): string { const content = JSON.stringify({ userEmail: data.userEmail, action: data.action, resourceType: data.resourceType, resourceId: data.resourceId, endpoint: data.endpoint, createdAt: data.createdAt.toISOString(), previousHash: data.previousHash || '', }); return crypto.createHash('sha256').update(content).digest('hex'); } /** * Bestimmt die Sensitivität basierend auf dem Ressourcentyp */ function determineSensitivity(resourceType: string): AuditSensitivity { const sensitivityMap: Record = { // CRITICAL Authentication: 'CRITICAL', BankCard: 'CRITICAL', IdentityDocument: 'CRITICAL', // HIGH Customer: 'HIGH', User: 'HIGH', CustomerConsent: 'HIGH', DataDeletionRequest: 'HIGH', // MEDIUM Contract: 'MEDIUM', Address: 'MEDIUM', Meter: 'MEDIUM', MeterReading: 'MEDIUM', StressfreiEmail: 'MEDIUM', CachedEmail: 'MEDIUM', // LOW Provider: 'LOW', Tariff: 'LOW', SalesPlatform: 'LOW', AppSetting: 'LOW', ContractCategory: 'LOW', CancellationPeriod: 'LOW', ContractDuration: 'LOW', }; return sensitivityMap[resourceType] || 'MEDIUM'; } /** * Prüft ob Änderungen verschlüsselt werden sollen. * Deaktiviert - sensible Felder werden bereits von der Prisma-Middleware als [REDACTED] gefiltert. */ function shouldEncryptChanges(_resourceType: string): boolean { return false; } /** * Erstellt einen neuen Audit-Log-Eintrag mit Hash-Kette */ export async function createAuditLog(data: CreateAuditLogData): Promise { try { // Letzten Hash abrufen für die Kette const lastLog = await prisma.auditLog.findFirst({ orderBy: { id: 'desc' }, select: { hash: true }, }); const previousHash = lastLog?.hash || null; const createdAt = new Date(); // Sensitivität bestimmen falls nicht angegeben const sensitivity = data.sensitivity || determineSensitivity(data.resourceType); // Änderungen serialisieren und ggf. verschlüsseln let changesBefore: string | null = null; let changesAfter: string | null = null; let changesEncrypted = false; if (data.changesBefore || data.changesAfter) { changesEncrypted = shouldEncryptChanges(data.resourceType); if (data.changesBefore) { const json = JSON.stringify(data.changesBefore); changesBefore = changesEncrypted ? encrypt(json) : json; } if (data.changesAfter) { const json = JSON.stringify(data.changesAfter); changesAfter = changesEncrypted ? encrypt(json) : json; } } // Hash generieren const hash = generateHash({ userEmail: data.userEmail, action: data.action, resourceType: data.resourceType, resourceId: data.resourceId, endpoint: data.endpoint, createdAt, previousHash, }); // Eintrag erstellen await prisma.auditLog.create({ data: { userId: data.userId, userEmail: data.userEmail, userRole: data.userRole, customerId: data.customerId, isCustomerPortal: data.isCustomerPortal || false, action: data.action, sensitivity, resourceType: data.resourceType, resourceId: data.resourceId, resourceLabel: data.resourceLabel, endpoint: data.endpoint, httpMethod: data.httpMethod, ipAddress: data.ipAddress, userAgent: data.userAgent, changesBefore, changesAfter, changesEncrypted, dataSubjectId: data.dataSubjectId, legalBasis: data.legalBasis, success: data.success ?? true, errorMessage: data.errorMessage, durationMs: data.durationMs, createdAt, hash, previousHash, }, }); } catch (error) { // Audit-Logging darf niemals die Hauptoperation blockieren console.error('[AuditService] Fehler beim Erstellen des Audit-Logs:', error); } } /** * Sucht Audit-Logs mit Filtern und Paginierung */ export async function searchAuditLogs(params: AuditLogSearchParams) { const { userId, customerId, dataSubjectId, action, sensitivity, resourceType, resourceId, startDate, endDate, success, search, page = 1, limit = 50, } = params; const where: Prisma.AuditLogWhereInput = {}; if (userId !== undefined) where.userId = userId; if (customerId !== undefined) where.customerId = customerId; if (dataSubjectId !== undefined) where.dataSubjectId = dataSubjectId; if (action) where.action = action; if (sensitivity) where.sensitivity = sensitivity; if (resourceType) where.resourceType = resourceType; if (resourceId) where.resourceId = resourceId; if (success !== undefined) where.success = success; if (startDate || endDate) { where.createdAt = {}; if (startDate) where.createdAt.gte = startDate; if (endDate) where.createdAt.lte = endDate; } if (search) { where.OR = [ { userEmail: { contains: search } }, { resourceLabel: { contains: search } }, { endpoint: { contains: search } }, ]; } const [logs, total] = await Promise.all([ prisma.auditLog.findMany({ where, orderBy: { createdAt: 'desc' }, skip: (page - 1) * limit, take: limit, }), prisma.auditLog.count({ where }), ]); // Entschlüsselung der Änderungen wenn nötig const decryptedLogs = logs.map((log) => ({ ...log, changesBefore: log.changesBefore && log.changesEncrypted ? JSON.parse(decrypt(log.changesBefore)) : log.changesBefore ? JSON.parse(log.changesBefore) : null, changesAfter: log.changesAfter && log.changesEncrypted ? JSON.parse(decrypt(log.changesAfter)) : log.changesAfter ? JSON.parse(log.changesAfter) : null, })); return { data: decryptedLogs, pagination: { page, limit, total, totalPages: Math.ceil(total / limit), }, }; } /** * Holt einen einzelnen Audit-Log-Eintrag */ export async function getAuditLogById(id: number) { const log = await prisma.auditLog.findUnique({ where: { id }, }); if (!log) return null; return { ...log, changesBefore: log.changesBefore && log.changesEncrypted ? JSON.parse(decrypt(log.changesBefore)) : log.changesBefore ? JSON.parse(log.changesBefore) : null, changesAfter: log.changesAfter && log.changesEncrypted ? JSON.parse(decrypt(log.changesAfter)) : log.changesAfter ? JSON.parse(log.changesAfter) : null, }; } /** * Holt alle Audit-Logs für eine betroffene Person (DSGVO) */ export async function getAuditLogsByDataSubject(customerId: number) { const logs = await prisma.auditLog.findMany({ where: { dataSubjectId: customerId }, orderBy: { createdAt: 'desc' }, }); return logs.map((log) => ({ ...log, changesBefore: log.changesBefore && log.changesEncrypted ? JSON.parse(decrypt(log.changesBefore)) : log.changesBefore ? JSON.parse(log.changesBefore) : null, changesAfter: log.changesAfter && log.changesEncrypted ? JSON.parse(decrypt(log.changesAfter)) : log.changesAfter ? JSON.parse(log.changesAfter) : null, })); } /** * Verifiziert die Integrität der Hash-Kette */ export async function verifyIntegrity(fromId?: number, toId?: number): Promise<{ valid: boolean; checkedCount: number; invalidEntries: number[]; }> { const where: Prisma.AuditLogWhereInput = {}; if (fromId !== undefined) where.id = { gte: fromId }; if (toId !== undefined) where.id = { ...(where.id as object || {}), lte: toId }; const logs = await prisma.auditLog.findMany({ where, orderBy: { id: 'asc' }, select: { id: true, userEmail: true, action: true, resourceType: true, resourceId: true, endpoint: true, createdAt: true, hash: true, previousHash: true, }, }); const invalidEntries: number[] = []; for (let i = 0; i < logs.length; i++) { const log = logs[i]; // Hash neu berechnen const expectedHash = generateHash({ userEmail: log.userEmail, action: log.action, resourceType: log.resourceType, resourceId: log.resourceId, endpoint: log.endpoint, createdAt: log.createdAt, previousHash: log.previousHash, }); // Prüfen ob Hash übereinstimmt if (log.hash !== expectedHash) { invalidEntries.push(log.id); continue; } // Prüfen ob previousHash mit dem Hash des vorherigen Eintrags übereinstimmt if (i > 0) { const previousLog = logs[i - 1]; if (log.previousHash !== previousLog.hash) { invalidEntries.push(log.id); } } } return { valid: invalidEntries.length === 0, checkedCount: logs.length, invalidEntries, }; } /** * Hash-Kette komplett neu berechnen (Reparatur) */ export async function rehashAll(): Promise<{ rehashedCount: number }> { const logs = await prisma.auditLog.findMany({ orderBy: { id: 'asc' }, select: { id: true, userEmail: true, action: true, resourceType: true, resourceId: true, endpoint: true, createdAt: true, }, }); let previousHash: string | null = null; let count = 0; for (const log of logs) { const hash = generateHash({ userEmail: log.userEmail, action: log.action, resourceType: log.resourceType, resourceId: log.resourceId, endpoint: log.endpoint, createdAt: log.createdAt, previousHash, }); await prisma.auditLog.update({ where: { id: log.id }, data: { hash, previousHash }, }); previousHash = hash; count++; } return { rehashedCount: count }; } /** * Exportiert Audit-Logs als JSON oder CSV */ export async function exportAuditLogs( params: AuditLogSearchParams, format: 'json' | 'csv' = 'json' ): Promise { // Alle Logs ohne Paginierung const result = await searchAuditLogs({ ...params, limit: 100000, page: 1 }); const logs = result.data; if (format === 'json') { return JSON.stringify(logs, null, 2); } // CSV Export const headers = [ 'ID', 'Zeitstempel', 'Benutzer', 'Aktion', 'Ressource', 'Ressource-ID', 'Bezeichnung', 'Endpoint', 'IP-Adresse', 'Erfolg', 'Sensitivität', ]; const rows = logs.map((log) => [ log.id.toString(), log.createdAt.toISOString(), log.userEmail, log.action, log.resourceType, log.resourceId || '', log.resourceLabel || '', log.endpoint, log.ipAddress, log.success ? 'Ja' : 'Nein', log.sensitivity, ]); const csvContent = [ headers.join(';'), ...rows.map((row) => row.map((cell) => `"${cell.replace(/"/g, '""')}"`).join(';')), ].join('\n'); return csvContent; } /** * Löscht alte Audit-Logs basierend auf Retention-Policies * Hinweis: Diese Funktion sollte nur von einem autorisierten Admin-Prozess aufgerufen werden */ export async function runRetentionCleanup(): Promise<{ deletedCount: number; policies: Array<{ resourceType: string; sensitivity: string | null; deletedCount: number }>; }> { const policies = await prisma.auditRetentionPolicy.findMany({ where: { isActive: true }, }); const results: Array<{ resourceType: string; sensitivity: string | null; deletedCount: number }> = []; let totalDeleted = 0; for (const policy of policies) { const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - policy.retentionDays); const where: Prisma.AuditLogWhereInput = { createdAt: { lt: cutoffDate }, }; if (policy.resourceType !== '*') { where.resourceType = policy.resourceType; } if (policy.sensitivity) { where.sensitivity = policy.sensitivity; } const deleted = await prisma.auditLog.deleteMany({ where }); results.push({ resourceType: policy.resourceType, sensitivity: policy.sensitivity, deletedCount: deleted.count, }); totalDeleted += deleted.count; } return { deletedCount: totalDeleted, policies: results, }; } /** * Holt die Retention-Policies */ export async function getRetentionPolicies() { return prisma.auditRetentionPolicy.findMany({ orderBy: [{ resourceType: 'asc' }, { sensitivity: 'asc' }], }); } /** * Aktualisiert eine Retention-Policy */ export async function updateRetentionPolicy( id: number, data: { retentionDays?: number; description?: string; legalBasis?: string; isActive?: boolean } ) { return prisma.auditRetentionPolicy.update({ where: { id }, data, }); }