Neues Paket mail/internal/protolog (log/slog): SessionLogger loggt strukturierte Ereignisse einer Verbindung mit fester correlation_id und protocol über die gesamte Verbindungsdauer (Akzeptanzkriterium 1) — ein Logger mit logger==nil ist sicher benutzbar und loggt nichts (Rückwärtskompatibilität zu ING-01..ING-07, Logging ist opt-in wie TLS und Guard-Konfiguration). RedactCommandLine ersetzt bei sensiblen Kommandos (PASS, LOGIN, AUTH) alle Argumente vollständig durch [REDACTED] statt einzeln zu parsen (Akzeptanzkriterium 2). Reconstruct liest zeilenweise JSON-Logs und liefert ausschließlich die Einträge einer Korrelations-ID in Reihenfolge — das geforderte Diagnosewerkzeug (Akzeptanzkriterium 3). Alle drei Sessions loggen jetzt session_start/command (je empfangener Zeile, redigiert)/session_end. Nachrichteninhalte werden strukturell nie geloggt: SMTP-DATA-Body-Zeilen laufen durch eine eigene Leseschleife, die nicht durch den Kommando-Logpfad der Hauptschleife kommt: nur das Kommando DATA selbst erscheint im Log. Alle drei Pflichtprüfungen mit echten Nachweisen durchgeführt, jeweils in IMAP, POP3 und SMTP einzeln: Redaktion gegen den echten laufenden Server bestätigt (Klartextpasswort bzw. absichtlich eingebettetes Geheimnis im SMTP-Body erscheint nie im Log), zwei gemischte reale Sessions über dieselbe Korrelations-ID lückenlos rekonstruiert, Lasttest mit 100 Sessions mit/ohne Logging ohne relevante Durchsatzeinbuße. go build/go vet/golangci-lint clean, gesamtes Mail-Modul (~29 Pakete) regressionsfrei getestet.
92 lines
3.4 KiB
Go
92 lines
3.4 KiB
Go
package protolog
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"log/slog"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestRedactCommandLine_HidesCredentials ist Teil der geforderten
|
|
// Pflichtprüfung 1 (ING-08): Redaktion sensibler Felder.
|
|
func TestRedactCommandLine_HidesCredentials(t *testing.T) {
|
|
cases := []struct {
|
|
verb string
|
|
args []string
|
|
wantSafe bool // true: darf das geheimnis NICHT enthalten
|
|
secret string
|
|
}{
|
|
{"PASS", []string{"geheim123"}, true, "geheim123"},
|
|
{"LOGIN", []string{"alice", "geheim123"}, true, "geheim123"},
|
|
{"AUTH", []string{"PLAIN", "AGFsaWNlAGdlaGVpbTEyMw=="}, true, "AGFsaWNlAGdlaGVpbTEyMw=="},
|
|
{"STAT", nil, false, ""},
|
|
{"USER", []string{"alice"}, false, "alice"},
|
|
}
|
|
for _, tc := range cases {
|
|
out := RedactCommandLine(tc.verb, tc.args)
|
|
if tc.wantSafe && strings.Contains(out, tc.secret) {
|
|
t.Fatalf("%s: geheimnis im klartext gefunden: %q", tc.verb, out)
|
|
}
|
|
if !strings.HasPrefix(out, strings.ToUpper(tc.verb)) {
|
|
t.Fatalf("%s: kommandoname fehlt in redigierter zeile: %q", tc.verb, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSessionLogger_EventNeverContainsRawMessage stellt sicher, dass
|
|
// über die reguläre Event-API keine Nachrichteninhalte geloggt werden
|
|
// können, ohne dass der Aufrufer sie explizit (und damit sichtbar im
|
|
// Code) als Attribut übergibt — Event selbst fügt nie Rohinhalte hinzu.
|
|
func TestSessionLogger_EventNeverContainsRawMessage(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
logger := slog.New(slog.NewJSONHandler(&buf, nil))
|
|
sl := NewSessionLogger(logger, "pop3")
|
|
|
|
sl.Event(context.Background(), "command", slog.String("command", RedactCommandLine("PASS", []string{"geheim123"})))
|
|
|
|
if strings.Contains(buf.String(), "geheim123") {
|
|
t.Fatalf("passwort im log gefunden: %s", buf.String())
|
|
}
|
|
if !strings.Contains(buf.String(), sl.CorrelationID()) {
|
|
t.Fatalf("correlation_id fehlt im log: %s", buf.String())
|
|
}
|
|
}
|
|
|
|
// TestReconstruct_ReturnsOnlyMatchingSessionInOrder ist die geforderte
|
|
// Pflichtprüfung 2 (ING-08): eine komplette Session ist über die
|
|
// Korrelations-ID lückenlos rekonstruierbar, aus einem Log mit
|
|
// mehreren gemischten Sessions.
|
|
func TestReconstruct_ReturnsOnlyMatchingSessionInOrder(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
logger := slog.New(slog.NewJSONHandler(&buf, nil))
|
|
|
|
target := NewSessionLogger(logger, "imap")
|
|
other := NewSessionLogger(logger, "imap")
|
|
|
|
target.Event(context.Background(), "session_start", slog.String("remote_addr", "127.0.0.1:1"))
|
|
other.Event(context.Background(), "session_start", slog.String("remote_addr", "127.0.0.1:2"))
|
|
target.Event(context.Background(), "command", slog.String("command", "LOGIN [REDACTED]"))
|
|
other.Event(context.Background(), "command", slog.String("command", "SELECT INBOX"))
|
|
target.Event(context.Background(), "command", slog.String("command", "SELECT INBOX"))
|
|
target.Event(context.Background(), "session_end")
|
|
other.Event(context.Background(), "session_end")
|
|
|
|
entries, err := Reconstruct(&buf, target.CorrelationID())
|
|
if err != nil {
|
|
t.Fatalf("Reconstruct: %v", err)
|
|
}
|
|
if len(entries) != 4 {
|
|
t.Fatalf("erwartete 4 einträge für die zielsession, habe %d", len(entries))
|
|
}
|
|
wantMsgs := []string{"session_start", "command", "command", "session_end"}
|
|
for i, e := range entries {
|
|
if e.Msg != wantMsgs[i] {
|
|
t.Fatalf("eintrag %d: erwartete msg %q, habe %q", i, wantMsgs[i], e.Msg)
|
|
}
|
|
if e.CorrelationID != target.CorrelationID() {
|
|
t.Fatalf("eintrag %d gehört zur falschen session", i)
|
|
}
|
|
}
|
|
}
|