Fehlgeschlagener /auth/refresh (Replay/Brute-Force auf geraubte Refresh-Tokens) wurde als TOKEN_REFRESH/LOW geloggt und entging der Alarmierung - ein Angreifer konnte von /login auf /refresh ausweichen, um unter der LOGIN_FAILED-Schwelle zu bleiben. Audit-Actions speisen die Alert-Engine nicht (die zaehlt SecurityEvent via emit). Fix daher an zwei Ebenen: - Detection: refresh()-Catch emittiert TOKEN_REJECTED -> greift die bestehende Schwelle (>=3 TOKEN_REJECTED/5min/IP -> CRITICAL). Severity wie Access-Token: abgelaufen/revoked = LOW (kein Sofort-Alert), ungueltige Signatur/Manipulation = HIGH. auth.service reicht dafuer err.code REFRESH_EXPIRED/REFRESH_INVALID durch. "Kein Cookie" emittiert bewusst nicht (normaler Erstbesuch). - Audit-Triage: fehlgeschlagener Refresh -> Sensitivitaet HIGH statt LOW + Label "Token-Refresh abgelehnt". Action bleibt TOKEN_REFRESH (semantisch ein Refresh, kein Login). Verifiziert: tsx-Test abgelaufen->LOW, manipuliert/garbage->HIGH; tsc gruen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
481 lines
19 KiB
TypeScript
481 lines
19 KiB
TypeScript
import { Response, NextFunction } from 'express';
|
||
import { AuditAction } from '@prisma/client';
|
||
import { AuthRequest } from '../types/index.js';
|
||
import { createAuditLog } from '../services/audit.service.js';
|
||
import { getAuditContext, AuditContext } from './auditContext.js';
|
||
|
||
// Resource-Typ-Mapping basierend auf Route-Patterns
|
||
const RESOURCE_MAPPING: Record<string, { type: string; extractId?: (req: AuthRequest) => string | undefined }> = {
|
||
'/api/customers': { type: 'Customer', extractId: (req) => req.params.id || req.params.customerId },
|
||
'/api/customers/*/bank-cards': { type: 'BankCard', extractId: (req) => req.params.bankCardId },
|
||
'/api/customers/*/documents': { type: 'IdentityDocument', extractId: (req) => req.params.documentId },
|
||
'/api/customers/*/addresses': { type: 'Address', extractId: (req) => req.params.addressId },
|
||
'/api/customers/*/meters': { type: 'Meter', extractId: (req) => req.params.meterId },
|
||
'/api/customers/*/consents': { type: 'CustomerConsent', extractId: (req) => req.params.type },
|
||
'/api/contracts': { type: 'Contract', extractId: (req) => req.params.id },
|
||
'/api/contracts/*/history': { type: 'ContractHistoryEntry', extractId: (req) => req.params.entryId },
|
||
'/api/contracts/*/tasks': { type: 'ContractTask', extractId: (req) => req.params.taskId },
|
||
'/api/users': { type: 'User', extractId: (req) => req.params.id },
|
||
'/api/providers': { type: 'Provider', extractId: (req) => req.params.id },
|
||
'/api/tariffs': { type: 'Tariff', extractId: (req) => req.params.id },
|
||
'/api/platforms': { type: 'SalesPlatform', extractId: (req) => req.params.id },
|
||
'/api/contract-categories': { type: 'ContractCategory', extractId: (req) => req.params.id },
|
||
'/api/cancellation-periods': { type: 'CancellationPeriod', extractId: (req) => req.params.id },
|
||
'/api/contract-durations': { type: 'ContractDuration', extractId: (req) => req.params.id },
|
||
'/api/settings': { type: 'AppSetting', extractId: (req) => req.params.key },
|
||
'/api/email-providers': { type: 'EmailProviderConfig', extractId: (req) => req.params.id },
|
||
'/api/meters': { type: 'Meter', extractId: (req) => req.params.id || req.params.meterId },
|
||
'/api/upload': { type: 'Upload' },
|
||
'/api/email-logs': { type: 'EmailLog' },
|
||
'/api/auth': { type: 'Authentication' },
|
||
'/api/audit-logs': { type: 'AuditLog', extractId: (req) => req.params.id },
|
||
'/api/gdpr': { type: 'GDPR' },
|
||
};
|
||
|
||
// Routen die nicht geloggt werden sollen
|
||
const EXCLUDED_ROUTES = [
|
||
'/api/health',
|
||
'/api/uploads',
|
||
];
|
||
|
||
/**
|
||
* Bestimmt die Aktion basierend auf HTTP-Methode und Erfolg
|
||
*/
|
||
function determineAction(method: string, path: string, success: boolean): AuditAction {
|
||
// Spezielle Auth-Aktionen
|
||
if (path.includes('/auth/login')) {
|
||
return success ? 'LOGIN' : 'LOGIN_FAILED';
|
||
}
|
||
if (path.includes('/auth/logout')) {
|
||
return 'LOGOUT';
|
||
}
|
||
// Stiller Token-Refresh (Cookie) – kein interaktiver Login, eigene Action.
|
||
// Erfolg vs. Fehlschlag wird NICHT über die Action getrennt (semantisch beides
|
||
// ein Refresh), sondern downstream über success-Flag + Sensitivität (LOW/HIGH)
|
||
// und den TOKEN_REJECTED-SecurityEvent im Controller (Pentest R164-01).
|
||
if (path.includes('/auth/refresh')) {
|
||
return 'TOKEN_REFRESH';
|
||
}
|
||
|
||
// Standard CRUD-Aktionen
|
||
switch (method.toUpperCase()) {
|
||
case 'GET':
|
||
return 'READ';
|
||
case 'POST':
|
||
return 'CREATE';
|
||
case 'PUT':
|
||
case 'PATCH':
|
||
return 'UPDATE';
|
||
case 'DELETE':
|
||
return 'DELETE';
|
||
default:
|
||
return 'READ';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Findet den passenden Resource-Typ für einen Pfad
|
||
*/
|
||
function findResourceMapping(path: string): { type: string; extractId?: (req: AuthRequest) => string | undefined } | null {
|
||
// Exakte Matches zuerst prüfen
|
||
for (const [pattern, mapping] of Object.entries(RESOURCE_MAPPING)) {
|
||
// Konvertiere Pattern zu Regex
|
||
const regexPattern = pattern
|
||
.replace(/\*/g, '[^/]+')
|
||
.replace(/\//g, '\\/');
|
||
const regex = new RegExp(`^${regexPattern}(?:/|$)`);
|
||
|
||
if (regex.test(path)) {
|
||
return mapping;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Extrahiert die betroffene Kunden-ID für DSGVO-Tracking
|
||
*/
|
||
function extractDataSubjectId(req: AuthRequest): number | undefined {
|
||
// Aus Route-Parameter
|
||
const customerId = req.params.customerId || req.params.id;
|
||
if (customerId && req.path.includes('/customers')) {
|
||
return parseInt(customerId);
|
||
}
|
||
|
||
// Aus Request-Body (bei Create)
|
||
if (req.body?.customerId) {
|
||
return parseInt(req.body.customerId);
|
||
}
|
||
|
||
// Bei Kundenportal-Zugriff
|
||
if (req.user?.customerId) {
|
||
return req.user.customerId;
|
||
}
|
||
|
||
return undefined;
|
||
}
|
||
|
||
/**
|
||
* Extrahiert die IP-Adresse des Clients
|
||
*/
|
||
function getClientIp(req: AuthRequest): string {
|
||
const forwarded = req.headers['x-forwarded-for'];
|
||
if (typeof forwarded === 'string') {
|
||
return forwarded.split(',')[0].trim();
|
||
}
|
||
return req.socket.remoteAddress || 'unknown';
|
||
}
|
||
|
||
// Menschenlesbare Bezeichnungen für Resource-Typen
|
||
const RESOURCE_TYPE_LABELS: Record<string, string> = {
|
||
Customer: 'Kunde',
|
||
Contract: 'Vertrag',
|
||
BankCard: 'Bankverbindung',
|
||
IdentityDocument: 'Ausweis',
|
||
Address: 'Adresse',
|
||
Meter: 'Zähler',
|
||
MeterReading: 'Zählerstand',
|
||
User: 'Benutzer',
|
||
Provider: 'Anbieter',
|
||
Tariff: 'Tarif',
|
||
SalesPlatform: 'Vertriebsplattform',
|
||
ContractCategory: 'Vertragskategorie',
|
||
CancellationPeriod: 'Kündigungsfrist',
|
||
ContractDuration: 'Vertragslaufzeit',
|
||
EmailProviderConfig: 'E-Mail-Provider',
|
||
AppSetting: 'Einstellung',
|
||
CustomerConsent: 'Einwilligung',
|
||
ContractTask: 'Aufgabe',
|
||
ContractHistoryEntry: 'Vertragshistorie',
|
||
GDPR: 'Datenschutz',
|
||
Authentication: 'Anmeldung',
|
||
AuditLog: 'Audit-Protokoll',
|
||
StressfreiEmail: 'Stressfrei-E-Mail',
|
||
CachedEmail: 'E-Mail',
|
||
};
|
||
|
||
const ACTION_LABELS: Record<string, string> = {
|
||
CREATE: 'erstellt',
|
||
READ: 'aufgerufen',
|
||
UPDATE: 'aktualisiert',
|
||
DELETE: 'gelöscht',
|
||
EXPORT: 'exportiert',
|
||
ANONYMIZE: 'anonymisiert',
|
||
LOGIN: 'angemeldet',
|
||
LOGOUT: 'abgemeldet',
|
||
LOGIN_FAILED: 'Anmeldung fehlgeschlagen',
|
||
TOKEN_REFRESH: 'Sitzung verlängert',
|
||
};
|
||
|
||
/**
|
||
* Erzeugt ein menschenlesbares Label für den Audit-Log-Eintrag
|
||
*/
|
||
function generateHumanLabel(
|
||
action: AuditAction,
|
||
resourceType: string,
|
||
req: AuthRequest,
|
||
responseBody: unknown
|
||
): string {
|
||
const typeName = RESOURCE_TYPE_LABELS[resourceType] || resourceType;
|
||
const actionName = ACTION_LABELS[action] || action;
|
||
|
||
// Identifikator aus Response oder Request extrahieren
|
||
let identifier = '';
|
||
if (responseBody && typeof responseBody === 'object' && 'data' in responseBody) {
|
||
const data = (responseBody as { data: Record<string, unknown> }).data;
|
||
if (data) {
|
||
identifier =
|
||
(data.contractNumber as string) ||
|
||
(data.customerNumber as string) ||
|
||
(data.meterNumber as string) ||
|
||
(data.name as string) ||
|
||
(data.email as string) ||
|
||
(data.firstName && data.lastName ? `${data.firstName} ${data.lastName}` : '') ||
|
||
'';
|
||
}
|
||
}
|
||
|
||
// Spezial-Labels für bestimmte Endpunkte
|
||
const path = req.path;
|
||
|
||
// Auth
|
||
if (path.includes('/auth/login') || path.includes('/auth/customer-login')) {
|
||
const email = req.body?.email || '';
|
||
return action === 'LOGIN'
|
||
? `Benutzer ${email} hat sich angemeldet`
|
||
: `Anmeldung fehlgeschlagen für ${email}`;
|
||
}
|
||
if (path.includes('/auth/logout')) return 'Benutzer hat sich abgemeldet';
|
||
if (path.includes('/auth/refresh')) {
|
||
const failed = responseBody && typeof responseBody === 'object' && (responseBody as { success?: boolean }).success === false;
|
||
return failed ? 'Token-Refresh abgelehnt (ungültig/abgelaufen)' : 'Sitzung verlängert (Token erneuert)';
|
||
}
|
||
|
||
// Kunden-Operationen
|
||
if (resourceType === 'Customer') {
|
||
if (action === 'CREATE') return `Kunde ${identifier} angelegt`;
|
||
if (action === 'UPDATE') return `Kundendaten ${identifier} aktualisiert`;
|
||
if (action === 'DELETE') return `Kunde ${identifier} gelöscht`;
|
||
if (action === 'READ' && req.params.id) return `Kundendaten ${identifier} aufgerufen`;
|
||
if (action === 'READ') return 'Kundenliste aufgerufen';
|
||
}
|
||
|
||
// Verträge
|
||
if (resourceType === 'Contract') {
|
||
if (path.includes('/cockpit')) return 'Vertrags-Cockpit aufgerufen';
|
||
if (path.includes('/follow-up')) return `Folgevertrag für ${identifier} erstellt`;
|
||
if (path.includes('/snooze')) return `Vertrag ${identifier} zurückgestellt`;
|
||
if (path.includes('/password')) return `Passwort für Vertrag ${identifier} abgerufen`;
|
||
if (path.includes('/sip-credentials')) return 'SIP-Zugangsdaten abgerufen';
|
||
if (path.includes('/internet-credentials')) return `Internet-Zugangsdaten für Vertrag ${identifier} abgerufen`;
|
||
if (path.includes('/successor-meter')) return `Folgezähler zu Vertrag ${identifier} hinzugefügt`;
|
||
if (action === 'CREATE') return `Vertrag ${identifier} angelegt`;
|
||
if (action === 'UPDATE') return `Vertrag ${identifier} aktualisiert`;
|
||
if (action === 'DELETE') return `Vertrag ${identifier} gelöscht`;
|
||
if (action === 'READ' && req.params.id) return `Vertrag ${identifier} aufgerufen`;
|
||
if (action === 'READ') return 'Vertragsliste aufgerufen';
|
||
}
|
||
|
||
// Bankverbindungen
|
||
if (resourceType === 'BankCard') {
|
||
if (action === 'CREATE') return `Bankverbindung hinzugefügt`;
|
||
if (action === 'UPDATE') return `Bankverbindung aktualisiert`;
|
||
if (action === 'DELETE') return `Bankverbindung gelöscht`;
|
||
}
|
||
|
||
// Ausweise
|
||
if (resourceType === 'IdentityDocument') {
|
||
if (action === 'CREATE') return `Ausweis ${identifier} hinzugefügt`;
|
||
if (action === 'UPDATE') return `Ausweis ${identifier} aktualisiert`;
|
||
if (action === 'DELETE') return `Ausweis gelöscht`;
|
||
}
|
||
|
||
// Adressen
|
||
if (resourceType === 'Address') {
|
||
if (action === 'CREATE') return `Adresse hinzugefügt`;
|
||
if (action === 'UPDATE') return `Adresse aktualisiert`;
|
||
if (action === 'DELETE') return `Adresse gelöscht`;
|
||
}
|
||
|
||
// Zähler
|
||
if (resourceType === 'Meter') {
|
||
if (action === 'CREATE') return `Zähler ${identifier} angelegt`;
|
||
if (action === 'UPDATE') return `Zähler ${identifier} aktualisiert`;
|
||
if (action === 'DELETE') return `Zähler gelöscht`;
|
||
}
|
||
|
||
// Einwilligungen
|
||
if (resourceType === 'CustomerConsent') {
|
||
const consentType = req.params.consentType || '';
|
||
const consentLabels: Record<string, string> = {
|
||
DATA_PROCESSING: 'Datenverarbeitung',
|
||
MARKETING_EMAIL: 'E-Mail-Marketing',
|
||
MARKETING_PHONE: 'Telefonmarketing',
|
||
DATA_SHARING_PARTNER: 'Datenweitergabe',
|
||
};
|
||
const consentName = consentLabels[consentType] || consentType;
|
||
if (action === 'UPDATE') {
|
||
const status = req.body?.status;
|
||
return status === 'GRANTED'
|
||
? `Einwilligung "${consentName}" erteilt`
|
||
: `Einwilligung "${consentName}" widerrufen`;
|
||
}
|
||
if (action === 'READ') return 'Einwilligungen abgerufen';
|
||
}
|
||
|
||
// Benutzer
|
||
if (resourceType === 'User') {
|
||
if (action === 'CREATE') return `Benutzer ${identifier} angelegt`;
|
||
if (action === 'UPDATE') return `Benutzer ${identifier} aktualisiert`;
|
||
if (action === 'DELETE') return `Benutzer ${identifier} gelöscht`;
|
||
if (action === 'READ' && req.params.id) return `Benutzerdaten ${identifier} aufgerufen`;
|
||
if (action === 'READ') return 'Benutzerliste aufgerufen';
|
||
}
|
||
|
||
// Aufgaben
|
||
if (resourceType === 'ContractTask') {
|
||
if (path.includes('/complete')) return `Aufgabe als erledigt markiert`;
|
||
if (action === 'CREATE') return `Aufgabe erstellt`;
|
||
if (action === 'UPDATE') return `Aufgabe aktualisiert`;
|
||
if (action === 'DELETE') return `Aufgabe gelöscht`;
|
||
}
|
||
|
||
// E-Mail-Provider
|
||
if (resourceType === 'EmailProviderConfig') {
|
||
if (path.includes('/test-connection')) return `E-Mail-Provider Verbindungstest`;
|
||
if (path.includes('/provision')) return `E-Mail-Adresse provisioniert`;
|
||
if (action === 'CREATE') return `E-Mail-Provider ${identifier} angelegt`;
|
||
if (action === 'UPDATE') return `E-Mail-Provider ${identifier} aktualisiert`;
|
||
if (action === 'DELETE') return `E-Mail-Provider ${identifier} gelöscht`;
|
||
}
|
||
|
||
// GDPR
|
||
if (resourceType === 'GDPR') {
|
||
if (path.includes('/dashboard')) return 'DSGVO-Dashboard aufgerufen';
|
||
if (path.includes('/export')) return 'Kundendaten exportiert (DSGVO Art. 15)';
|
||
if (path.includes('/privacy-policy')) {
|
||
return action === 'UPDATE' ? 'Datenschutzerklärung aktualisiert' : 'Datenschutzerklärung aufgerufen';
|
||
}
|
||
if (path.includes('/authorization-template')) {
|
||
return action === 'UPDATE' ? 'Vollmacht-Vorlage aktualisiert' : 'Vollmacht-Vorlage aufgerufen';
|
||
}
|
||
if (path.includes('/send-consent-link')) return 'Datenschutz-Link versendet';
|
||
if (path.includes('/authorizations') && path.includes('/send')) return 'Vollmacht-Anfrage versendet';
|
||
if (path.includes('/authorizations') && path.includes('/grant')) return 'Vollmacht erteilt';
|
||
if (path.includes('/authorizations') && path.includes('/withdraw')) return 'Vollmacht widerrufen';
|
||
if (path.includes('/authorizations') && path.includes('/upload')) return 'Vollmacht-PDF hochgeladen';
|
||
if (path.includes('/authorizations') && path.includes('/document') && action === 'DELETE') return 'Vollmacht-PDF gelöscht';
|
||
if (path.includes('/my-privacy')) return 'Eigene Datenschutzseite aufgerufen';
|
||
if (path.includes('/my-consent-status')) return 'Eigener Einwilligungsstatus geprüft';
|
||
if (path.includes('/my-authorizations')) return 'Eigene Vollmachten aufgerufen';
|
||
if (path.includes('/deletions')) {
|
||
if (action === 'CREATE') return 'Löschanfrage erstellt';
|
||
if (path.includes('/process')) return 'Löschanfrage bearbeitet';
|
||
return 'Löschanfragen aufgerufen';
|
||
}
|
||
if (path.includes('/consent-status')) return 'Einwilligungsstatus geprüft';
|
||
if (path.includes('/consents/overview')) return 'Einwilligungsübersicht aufgerufen';
|
||
}
|
||
|
||
// Einstellungen
|
||
if (resourceType === 'AppSetting') {
|
||
if (action === 'UPDATE') return `Einstellung "${req.params.key || ''}" geändert`;
|
||
if (action === 'READ') return 'Einstellungen aufgerufen';
|
||
}
|
||
|
||
// Zähler-Readings
|
||
if (path.includes('/readings')) {
|
||
if (path.includes('/report')) return 'Zählerstand vom Kunden gemeldet';
|
||
if (path.includes('/transfer')) return 'Zählerstand als übertragen markiert';
|
||
if (action === 'CREATE') return 'Zählerstand erfasst';
|
||
if (action === 'UPDATE') return 'Zählerstand aktualisiert';
|
||
if (action === 'DELETE') return 'Zählerstand gelöscht';
|
||
}
|
||
|
||
// Upload-Operationen
|
||
if (path.includes('/upload') || path.includes('/privacy-policy')) {
|
||
if (path.includes('/privacy-policy') && action === 'DELETE') return 'Datenschutzerklärung-PDF gelöscht';
|
||
if (path.includes('/privacy-policy')) return 'Datenschutzerklärung-PDF hochgeladen';
|
||
}
|
||
|
||
// Standard-Fallback
|
||
if (identifier) {
|
||
return `${typeName} ${identifier} ${actionName}`;
|
||
}
|
||
return `${typeName} ${actionName}`;
|
||
}
|
||
|
||
/**
|
||
* Audit Middleware - loggt alle API-Aufrufe asynchron
|
||
*/
|
||
export function auditMiddleware(req: AuthRequest, res: Response, next: NextFunction): void {
|
||
const startTime = Date.now();
|
||
|
||
// Ausgeschlossene Routen überspringen
|
||
if (EXCLUDED_ROUTES.some((route) => req.path.startsWith(route))) {
|
||
next();
|
||
return;
|
||
}
|
||
|
||
// Resource-Mapping finden
|
||
const mapping = findResourceMapping(req.path);
|
||
if (!mapping) {
|
||
// Unbekannte Route - trotzdem loggen mit generischem Typ
|
||
next();
|
||
return;
|
||
}
|
||
|
||
// Original res.json überschreiben um Response zu capturen
|
||
const originalJson = res.json.bind(res);
|
||
let responseBody: unknown = null;
|
||
let responseSuccess = true;
|
||
|
||
res.json = function (body: unknown) {
|
||
responseBody = body;
|
||
if (typeof body === 'object' && body !== null && 'success' in body) {
|
||
responseSuccess = (body as { success: boolean }).success;
|
||
}
|
||
return originalJson(body);
|
||
};
|
||
|
||
// Response-Ende abfangen für Logging
|
||
// Audit-Kontext hier erfassen (bevor AsyncLocalStorage den Kontext verliert)
|
||
let capturedAuditContext: ReturnType<typeof getAuditContext> | undefined;
|
||
|
||
const origEnd = res.end;
|
||
(res as any).end = function(chunk?: any, encoding?: any, cb?: any) {
|
||
// Kontext VOR dem Ende erfassen
|
||
capturedAuditContext = getAuditContext();
|
||
return origEnd.call(this, chunk, encoding, cb);
|
||
};
|
||
|
||
res.on('finish', () => {
|
||
// Async Logging - blockiert nicht die Response
|
||
setImmediate(async () => {
|
||
try {
|
||
const durationMs = Date.now() - startTime;
|
||
const action = determineAction(req.method, req.path, responseSuccess);
|
||
|
||
// READ-Aktionen nicht loggen (nur Änderungen, Logins und Exporte)
|
||
if (action === 'READ') return;
|
||
|
||
// Routen die bereits gezielt via logChange() geloggt werden → nicht doppelt loggen
|
||
const manuallyLoggedPaths = [
|
||
'/api/customers',
|
||
'/api/contracts',
|
||
'/api/meters',
|
||
'/api/gdpr',
|
||
'/api/upload',
|
||
];
|
||
// Login/Logout immer loggen
|
||
if (action !== 'LOGIN' && action !== 'LOGOUT' && action !== 'LOGIN_FAILED') {
|
||
if (manuallyLoggedPaths.some(p => req.originalUrl?.startsWith(p) || req.baseUrl?.startsWith(p))) return;
|
||
}
|
||
|
||
const resourceId = mapping.extractId?.(req);
|
||
const dataSubjectId = extractDataSubjectId(req);
|
||
|
||
// Audit-Kontext nutzen (wurde vor Response-Ende erfasst)
|
||
const auditContext = capturedAuditContext;
|
||
|
||
// Menschenlesbares Label generieren
|
||
const resourceLabel = generateHumanLabel(action, mapping.type, req, responseBody);
|
||
|
||
await createAuditLog({
|
||
userId: req.user?.userId,
|
||
userEmail: req.user?.email || 'anonymous',
|
||
userRole: req.user?.isCustomerPortal ? 'Kundenportal' : (req.user as any)?.roleName || 'Mitarbeiter',
|
||
customerId: req.user?.customerId,
|
||
isCustomerPortal: req.user?.isCustomerPortal,
|
||
action,
|
||
// Erfolgreicher Token-Refresh ist Routine → LOW statt CRITICAL (sonst Log-Flut).
|
||
// Fehlgeschlagener Refresh (Replay/Brute-Force-Verdacht) → HIGH, damit er in
|
||
// der Triage nicht neben legitimen Refreshes untergeht (Pentest R164-01).
|
||
// Andere Auth-Events behalten ihre Default-Sensitivität (Authentication → CRITICAL).
|
||
sensitivity: action === 'TOKEN_REFRESH' ? (responseSuccess ? 'LOW' : 'HIGH') : undefined,
|
||
resourceType: mapping.type,
|
||
resourceId,
|
||
resourceLabel,
|
||
endpoint: req.path,
|
||
httpMethod: req.method,
|
||
ipAddress: getClientIp(req),
|
||
userAgent: req.headers['user-agent'],
|
||
changesBefore: auditContext?.before,
|
||
changesAfter: auditContext?.after,
|
||
dataSubjectId,
|
||
success: responseSuccess,
|
||
errorMessage: !responseSuccess && responseBody && typeof responseBody === 'object' && 'error' in responseBody
|
||
? (responseBody as { error: string }).error
|
||
: undefined,
|
||
durationMs,
|
||
});
|
||
} catch (error) {
|
||
// Audit-Logging darf niemals die Anwendung beeinträchtigen
|
||
console.error('[AuditMiddleware] Fehler beim Logging:', error);
|
||
}
|
||
});
|
||
});
|
||
|
||
next();
|
||
}
|