Audit: Portaldaten-Opt-out als eigenes CRITICAL-Event loggen

Auf Wunsch des Pentesters (R117-Nachtrag). Das Umschalten des
portalCredentialsNotRequired-Flags emittiert jetzt zusätzlich zum
generischen Contract-Update-Diff ein dediziertes UPDATE-Event
unter resourceType 'ContractPassword' – landet damit in derselben
CRITICAL-Sensitivity-Reihe wie Klartext-Password-Reads.

Motivation: das Setzen des Flags räumt server-seitig
portalUsername, portalPasswordEncrypted und stressfreiEmailId auf
NULL. Diese Löschung soll unabhängig vom generischen Diff sofort
sichtbar sein. Rücknahme wird ebenfalls geloggt.

Details enthält alte + neue Flag-Werte plus Bool-Marker, welche
Anmeldedaten vor dem Opt-out belegt waren – keine Klartext-
Leckage im Log.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 14:58:51 +02:00
co-authored by Claude Opus 4.7
parent dcdb3cb057
commit 66fb5092fe
2 changed files with 48 additions and 0 deletions
@@ -235,6 +235,8 @@ export async function updateContract(req: AuthRequest, res: Response): Promise<v
cancellationDate: 'Kündigungsdatum', cancellationSentDate: 'Kündigung gesendet am',
identityDocumentId: 'Ausweis', bankCardId: 'Bankverbindung', addressId: 'Adresse',
commission: 'Provision', notes: 'Notizen',
portalCredentialsNotRequired: 'Portaldaten-Opt-out',
stressfreiEmailId: 'Stressfrei-/Corporate-Adresse',
};
const energyLabels: Record<string, string> = {
meterId: 'Zähler', maloId: 'MaLo-ID', annualConsumption: 'Jahresverbrauch',
@@ -280,6 +282,39 @@ export async function updateContract(req: AuthRequest, res: Response): Promise<v
customerId: before?.customerId,
});
// Zusätzliches CRITICAL-Event für das „Portaldaten nicht benötigt"-Opt-out.
// Landet unter resourceType ContractPassword (sensitivity CRITICAL laut
// audit.service determineSensitivity) und liegt damit in derselben
// DSGVO-Reihe wie Klartext-Password-Reads. Motivation: das Setzen des
// Flags räumt server-seitig portalUsername/portalPasswordEncrypted/
// stressfreiEmailId auf NULL diese Löschung soll unabhängig vom
// generischen UPDATE-Diff nachweisbar sein. Rücknahme des Flags wird
// ebenfalls geloggt, damit die Kette in beide Richtungen konsistent ist.
if (
before
&& 'portalCredentialsNotRequired' in body
&& before.portalCredentialsNotRequired !== body.portalCredentialsNotRequired
) {
const toggledOn = body.portalCredentialsNotRequired === true;
await logChange({
req,
action: 'UPDATE',
resourceType: 'ContractPassword',
resourceId: contractId.toString(),
label: toggledOn
? `Vertrag ${before.contractNumber || contractId}: Portal-Zugangsdaten als „nicht erforderlich" markiert (Username/Passwort/Stressfrei-ID auf NULL gesetzt)`
: `Vertrag ${before.contractNumber || contractId}: „Portaldaten nicht erforderlich"-Marker zurückgenommen`,
details: {
vorher: before.portalCredentialsNotRequired,
nachher: body.portalCredentialsNotRequired,
hatUsernameVorher: !!before.portalUsername,
hatPasswortVorher: !!before.portalPasswordEncrypted,
hatStressfreiVorher: !!before.stressfreiEmailId,
},
customerId: before.customerId,
});
}
// Response sanitisieren sonst leakt portalPasswordEncrypted etc.
// (Pentest Runde 15, gleiche Klasse wie 20.3 für Customer).
const isPortal = !!req.user?.isCustomerPortal;
+13
View File
@@ -97,6 +97,19 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
## ✅ Erledigt
- [x] **📝 DSGVO-Audit: Portaldaten-Opt-out als eigenes CRITICAL-Event**
- Auf Wunsch des Pentesters (R117-Nachtrag): das Umschalten des
`portalCredentialsNotRequired`-Flags emittiert jetzt zusätzlich zum
generischen Contract-Update-Diff ein dediziertes `UPDATE`-Event
unter `resourceType: 'ContractPassword'` — landet damit in derselben
CRITICAL-Sensitivity-Reihe wie Klartext-Password-Reads.
- Motivation: das Setzen des Flags räumt server-seitig
`portalUsername`/`portalPasswordEncrypted`/`stressfreiEmailId` auf
`NULL`. Diese Löschung sensitiver Daten ist jetzt unabhängig vom
generischen Diff nachweisbar. Rücknahme wird ebenfalls geloggt.
- `details` enthält alte + neue Flag-Werte plus Bool-Marker, welche
Anmeldedaten vor dem Opt-out belegt waren (keine Klartext-Leckage).
- [x] **🔕 Vertrag: „Portaldaten nicht möglich / nicht benötigt"-Opt-out**
- Dritter Radio-Button in der `Zugangsdaten`-Card des Vertragsformulars:
„Nicht möglich zu bekommen / nicht benötigt". Wenn gesetzt, unterdrückt