import { Response } from 'express'; import { AuthRequest } from '../types/index.js'; import * as auditService from '../services/audit.service.js'; import { logChange } from '../services/audit.service.js'; import { AuditAction, AuditSensitivity } from '@prisma/client'; /** * Audit-Logs mit Filtern abrufen */ export async function getAuditLogs(req: AuthRequest, res: Response) { try { const { userId, customerId, dataSubjectId, action, sensitivity, resourceType, resourceId, startDate, endDate, success, search, page, limit, } = req.query; const result = await auditService.searchAuditLogs({ userId: userId ? parseInt(userId as string) : undefined, customerId: customerId ? parseInt(customerId as string) : undefined, dataSubjectId: dataSubjectId ? parseInt(dataSubjectId as string) : undefined, action: action as AuditAction | undefined, sensitivity: sensitivity as AuditSensitivity | undefined, resourceType: resourceType as string | undefined, resourceId: resourceId as string | undefined, startDate: startDate ? new Date(startDate as string) : undefined, endDate: endDate ? new Date(endDate as string) : undefined, success: success !== undefined ? success === 'true' : undefined, search: search as string | undefined, page: page ? parseInt(page as string) : 1, limit: limit ? parseInt(limit as string) : 50, }); res.json({ success: true, ...result }); } catch (error) { console.error('Fehler beim Abrufen der Audit-Logs:', error); res.status(500).json({ success: false, error: 'Fehler beim Abrufen der Audit-Logs' }); } } /** * Einzelnes Audit-Log abrufen */ export async function getAuditLogById(req: AuthRequest, res: Response) { try { const id = parseInt(req.params.id); // Nicht-numerische :id (z.B. GET /audit-logs/verify statt POST /verify) // landete sonst als NaN in Prisma findUnique → 500. Sauberer 400. // (Pentest R120 – gleiche „400 statt 500"-Klasse wie R64.1/R104.1.) if (Number.isNaN(id)) { return res.status(400).json({ success: false, error: 'Ungültige Audit-Log-ID' }); } const log = await auditService.getAuditLogById(id); if (!log) { return res.status(404).json({ success: false, error: 'Audit-Log nicht gefunden' }); } res.json({ success: true, data: log }); } catch (error) { console.error('Fehler beim Abrufen des Audit-Logs:', error); res.status(500).json({ success: false, error: 'Fehler beim Abrufen des Audit-Logs' }); } } /** * Audit-Logs für einen Kunden abrufen (DSGVO) */ export async function getAuditLogsByCustomer(req: AuthRequest, res: Response) { try { const customerId = parseInt(req.params.customerId); if (Number.isNaN(customerId)) { return res.status(400).json({ success: false, error: 'Ungültige Kunden-ID' }); } const logs = await auditService.getAuditLogsByDataSubject(customerId); res.json({ success: true, data: logs }); } catch (error) { console.error('Fehler beim Abrufen der Kunden-Audit-Logs:', error); res.status(500).json({ success: false, error: 'Fehler beim Abrufen der Audit-Logs' }); } } /** * Audit-Logs exportieren */ export async function exportAuditLogs(req: AuthRequest, res: Response) { try { const format = (req.query.format as 'json' | 'csv') || 'json'; const { action, sensitivity, resourceType, startDate, endDate, } = req.query; const content = await auditService.exportAuditLogs( { action: action as AuditAction | undefined, sensitivity: sensitivity as AuditSensitivity | undefined, resourceType: resourceType as string | undefined, startDate: startDate ? new Date(startDate as string) : undefined, endDate: endDate ? new Date(endDate as string) : undefined, }, format ); if (format === 'csv') { const filename = `audit-logs-${new Date().toISOString().split('T')[0]}.csv`; res.setHeader('Content-Type', 'text/csv; charset=utf-8'); res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); // BOM für Excel UTF-8 Erkennung res.send('\uFEFF' + content); } else { res.json({ success: true, data: JSON.parse(content) }) } } catch (error) { console.error('Fehler beim Exportieren der Audit-Logs:', error); res.status(500).json({ success: false, error: 'Fehler beim Exportieren' }); } } /** * Hash-Ketten-Integrität prüfen */ export async function verifyIntegrity(req: AuthRequest, res: Response) { try { const { fromId, toId } = req.query; const result = await auditService.verifyIntegrity( fromId ? parseInt(fromId as string) : undefined, toId ? parseInt(toId as string) : undefined ); // Zwei sehr unterschiedliche Befunde sauber trennen – vorher wurde beides // pauschal als "manipuliert" gemeldet, was strukturelle Lücken wie einen // echten Angriff aussehen liess (und damit die Meldung entwertete). const tampered = result.tamperedEntries.length; const gaps = result.chainGaps.length; const unexplained = result.unexplainedGaps.length; const luecken = gaps > 0 ? `${gaps} strukturelle Lücken` + (unexplained === 0 ? ' (alle durch protokollierte Löschungen erklärt)' : unexplained < gaps ? `, davon ${unexplained} ohne dokumentierte Löschung` : ' ohne dokumentierte Löschung') : ''; const unverifiable = result.unverifiableEntries.length; const keinSchluessel = unverifiable > 0 ? ` ${unverifiable} Einträge sind HMAC-signiert und ohne konfigurierten AUDIT_HMAC_KEY nicht prüfbar.` : ''; const message = tampered > 0 ? `${tampered} MANIPULIERTE Einträge gefunden` + (gaps > 0 ? ` (zusätzlich ${luecken})` : '') : gaps > 0 ? `Keine Manipulation. ${luecken} – Inhalte unverändert.` : 'Alle Einträge sind unverändert und lückenlos verkettet'; res.json({ success: true, data: { valid: result.valid, checkedCount: result.checkedCount, invalidEntries: result.invalidEntries, // Ernst: Inhalt einer bestehenden Zeile wurde nachträglich verändert. tamperedEntries: result.tamperedEntries, // Meist harmlos: Verkettung unterbrochen, Inhalte selbst unversehrt. chainGaps: result.chainGaps, // Nur Lücken ohne protokollierte Löschung sind erklärungsbedürftig. unexplainedGaps: result.unexplainedGaps, tampered: tampered > 0, message: message + keinSchluessel, unverifiableEntries: result.unverifiableEntries, }, }); } catch (error) { console.error('Fehler bei der Integritätsprüfung:', error); res.status(500).json({ success: false, error: 'Fehler bei der Integritätsprüfung' }); } } /** * Hash-Kette reparieren (alle Hashes neu berechnen) */ export async function rehashAll(req: AuthRequest, res: Response) { try { // Ausdrueckliche Bestaetigung verlangen (Pentest R170-01). // // Ein Rehash macht die Kette rechnerisch stimmig und setzt damit die // Beweiskraft der Vergangenheit zurueck - das darf kein Nebeneffekt eines // versehentlichen oder tastenden POST sein. Genau so wurde der Endpunkt // bei einer Methoden-Erkundung unbeabsichtigt ausgeloest. if (req.body?.confirm !== 'REHASH') { res.status(400).json({ success: false, error: 'Rehash setzt die Beweiskraft der bestehenden Einträge zurück und ist nicht ' + 'umkehrbar. Zum Bestätigen {"confirm":"REHASH"} mitsenden.', }); return; } const result = await auditService.rehashAll({ userEmail: req.user?.email, ipAddress: req.ip || (req.socket as any)?.remoteAddress, }); res.json({ success: true, data: result, message: `${result.rehashedCount} Einträge neu gehasht. Kette ist jetzt intakt.`, }); } catch (error) { console.error('Fehler beim Re-Hashing:', error); res.status(500).json({ success: false, error: 'Fehler beim Re-Hashing' }); } } /** * Retention-Policies abrufen */ export async function getRetentionPolicies(req: AuthRequest, res: Response) { try { const policies = await auditService.getRetentionPolicies(); res.json({ success: true, data: policies }); } catch (error) { console.error('Fehler beim Abrufen der Retention-Policies:', error); res.status(500).json({ success: false, error: 'Fehler beim Abrufen der Policies' }); } } /** * Retention-Policy aktualisieren */ export async function updateRetentionPolicy(req: AuthRequest, res: Response) { try { const id = parseInt(req.params.id); if (Number.isNaN(id)) { return res.status(400).json({ success: false, error: 'Ungültige Policy-ID' }); } const { retentionDays, description, legalBasis, isActive } = req.body; const policy = await auditService.updateRetentionPolicy(id, { retentionDays, description, legalBasis, isActive, }); await logChange({ req, action: 'UPDATE', resourceType: 'RetentionPolicy', resourceId: id.toString(), label: `Aufbewahrungsrichtlinie aktualisiert`, }); res.json({ success: true, data: policy }); } catch (error) { console.error('Fehler beim Aktualisieren der Retention-Policy:', error); res.status(500).json({ success: false, error: 'Fehler beim Aktualisieren' }); } } /** * Retention-Cleanup manuell ausführen */ export async function runRetentionCleanup(req: AuthRequest, res: Response) { try { // Auch hier ausdrueckliche Bestaetigung: Cleanup loescht Audit-Zeilen // endgueltig und reisst dabei die Kette auf (Pentest R170-01). if (req.body?.confirm !== 'CLEANUP') { res.status(400).json({ success: false, error: 'Cleanup löscht Audit-Einträge endgültig. Zum Bestätigen ' + '{"confirm":"CLEANUP"} mitsenden.', }); return; } const result = await auditService.runRetentionCleanup(); res.json({ success: true, data: result, message: `${result.deletedCount} alte Audit-Logs wurden gelöscht`, }); } catch (error) { console.error('Fehler beim Retention-Cleanup:', error); res.status(500).json({ success: false, error: 'Fehler beim Cleanup' }); } }