Files
Strix/internal/utils/logger/masking_handler_test.go
T
eduard256 3acc966658 Mask URL-encoded passwords in debug logs
SecretStore.Add now registers both plain text and URL-encoded forms
of the password. Fixes cases where passwords with special characters
(e.g. @, #, :) appear percent-encoded in URLs but were not matched
by the masking handler.
2026-03-20 19:59:22 +00:00

263 lines
7.7 KiB
Go

package logger
import (
"bytes"
"context"
"errors"
"log/slog"
"strings"
"sync"
"testing"
)
func TestSecretStore_AddRemoveMask(t *testing.T) {
store := NewSecretStore()
// No secrets: text unchanged
if got := store.Mask("password=secret123"); got != "password=secret123" {
t.Errorf("expected unchanged text, got %q", got)
}
// Add a secret
store.Add("secret123")
if got := store.Mask("password=secret123"); got != "password=***" {
t.Errorf("expected masked, got %q", got)
}
// Remove the secret
store.Remove("secret123")
if got := store.Mask("password=secret123"); got != "password=secret123" {
t.Errorf("expected unmasked after remove, got %q", got)
}
}
func TestSecretStore_EmptyString(t *testing.T) {
store := NewSecretStore()
store.Add("")
if got := store.Mask("test"); got != "test" {
t.Errorf("empty secret should be ignored, got %q", got)
}
store.Remove("") // should not panic
}
func TestSecretStore_MultipleSecrets(t *testing.T) {
store := NewSecretStore()
store.Add("pass1")
store.Add("pass2")
got := store.Mask("url=rtsp://user:pass1@host and also pwd=pass2&rate=0")
if strings.Contains(got, "pass1") || strings.Contains(got, "pass2") {
t.Errorf("both passwords should be masked, got %q", got)
}
}
func TestSecretStore_ConcurrentAccess(t *testing.T) {
store := NewSecretStore()
var wg sync.WaitGroup
// Simulate concurrent scans adding/removing/masking
for i := 0; i < 100; i++ {
wg.Add(3)
secret := "secret" + string(rune('A'+i%26))
go func(s string) {
defer wg.Done()
store.Add(s)
}(secret)
go func() {
defer wg.Done()
_ = store.Mask("some text with secretA in it")
}()
go func(s string) {
defer wg.Done()
store.Remove(s)
}(secret)
}
wg.Wait()
}
func TestSecretMaskingHandler_MasksStringAttrs(t *testing.T) {
var buf bytes.Buffer
store := NewSecretStore()
store.Add("mypassword")
inner := slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})
handler := NewSecretMaskingHandler(inner, store)
log := slog.New(handler)
log.Debug("testing stream", "url", "rtsp://admin:mypassword@192.168.1.10/stream")
output := buf.String()
if strings.Contains(output, "mypassword") {
t.Errorf("password should be masked in output: %s", output)
}
if !strings.Contains(output, "***") {
t.Errorf("expected *** in output: %s", output)
}
}
func TestSecretMaskingHandler_MasksMessage(t *testing.T) {
var buf bytes.Buffer
store := NewSecretStore()
store.Add("secretpwd")
inner := slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})
handler := NewSecretMaskingHandler(inner, store)
log := slog.New(handler)
log.Debug("failed with secretpwd in message")
output := buf.String()
if strings.Contains(output, "secretpwd") {
t.Errorf("password should be masked in message: %s", output)
}
}
func TestSecretMaskingHandler_MasksErrorValues(t *testing.T) {
var buf bytes.Buffer
store := NewSecretStore()
store.Add("r6wnm0wlix")
inner := slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})
handler := NewSecretMaskingHandler(inner, store)
log := slog.New(handler)
err := errors.New(`Get "http://10.0.20.111/cgi-bin/encoder?PWD=r6wnm0wlix&USER=admin": dial tcp`)
log.Debug("request failed", "error", err)
output := buf.String()
if strings.Contains(output, "r6wnm0wlix") {
t.Errorf("password should be masked in error: %s", output)
}
}
func TestSecretMaskingHandler_NoSecretsPassthrough(t *testing.T) {
var buf bytes.Buffer
store := NewSecretStore()
inner := slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})
handler := NewSecretMaskingHandler(inner, store)
log := slog.New(handler)
log.Debug("normal message", "key", "value")
output := buf.String()
if !strings.Contains(output, "normal message") || !strings.Contains(output, "value") {
t.Errorf("output should pass through unchanged: %s", output)
}
}
func TestSecretMaskingHandler_MasksMultipleOccurrences(t *testing.T) {
var buf bytes.Buffer
store := NewSecretStore()
store.Add("secret123")
inner := slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})
handler := NewSecretMaskingHandler(inner, store)
log := slog.New(handler)
log.Debug("test",
"url1", "rtsp://user:secret123@host1/stream",
"url2", "http://host2/snap?pwd=secret123",
"path", "/user=admin_password=secret123_channel=1",
)
output := buf.String()
if strings.Contains(output, "secret123") {
t.Errorf("all occurrences should be masked: %s", output)
}
}
func TestSecretMaskingHandler_Enabled(t *testing.T) {
store := NewSecretStore()
inner := slog.NewTextHandler(&bytes.Buffer{}, &slog.HandlerOptions{Level: slog.LevelInfo})
handler := NewSecretMaskingHandler(inner, store)
if handler.Enabled(context.Background(), slog.LevelDebug) {
t.Error("debug should be disabled when level is info")
}
if !handler.Enabled(context.Background(), slog.LevelInfo) {
t.Error("info should be enabled")
}
}
func TestSecretMaskingHandler_SpecialCharsPassword(t *testing.T) {
var buf bytes.Buffer
store := NewSecretStore()
store.Add("p@ss:w0rd#1")
inner := slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})
handler := NewSecretMaskingHandler(inner, store)
log := slog.New(handler)
// Simulate URLs built by builder.go and onvif_simple.go
// 1. RTSP with url.QueryEscape (onvif_simple.go:395)
log.Debug("testing RTSP stream", "url", "rtsp://admin:p%40ss%3Aw0rd%231@192.168.1.10:554/stream1")
// 2. HTTP with url.UserPassword (builder.go:355) -- Go encodes special chars
log.Debug("testing HTTP stream", "url", "http://admin:p%40ss%3Aw0rd%231@192.168.1.10/snap.jpg")
// 3. Query params with url.Values.Encode (builder.go:377)
log.Debug("testing HTTP stream", "url", "http://192.168.1.10/snap.jpg?pwd=p%40ss%3Aw0rd%231&user=admin")
// 4. Error from Go http.Client (contains encoded URL)
log.Debug("stream test failed",
"url", "http://admin:p%40ss%3Aw0rd%231@192.168.1.10/camera",
"error", `HTTP request failed: Get "http://admin:***@192.168.1.10/camera": connection refused`)
output := buf.String()
t.Logf("Output:\n%s", output)
if strings.Contains(output, "p@ss:w0rd#1") {
t.Errorf("plain text password should be masked: %s", output)
}
if strings.Contains(output, "p%40ss%3Aw0rd%231") {
t.Errorf("URL-encoded password should be masked: %s", output)
}
}
func TestSecretMaskingHandler_PlainPassword(t *testing.T) {
var buf bytes.Buffer
store := NewSecretStore()
store.Add("simplepass123")
inner := slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})
handler := NewSecretMaskingHandler(inner, store)
log := slog.New(handler)
// Plain password without special chars -- no encoding difference
log.Debug("testing RTSP stream", "url", "rtsp://admin:simplepass123@192.168.1.10:554/stream")
log.Debug("testing HTTP stream", "url", "http://192.168.1.10/snap.jpg?pwd=simplepass123&user=admin")
log.Debug("stream test failed",
"url", "http://admin:simplepass123@192.168.1.10/camera",
"error", `HTTP request failed: Get "http://192.168.1.10/snap.jpg?pwd=simplepass123&user=admin": connection refused`)
output := buf.String()
t.Logf("Output:\n%s", output)
if strings.Contains(output, "simplepass123") {
t.Errorf("password should be masked everywhere: %s", output)
}
}
func TestSecretMaskingHandler_WithAttrs(t *testing.T) {
var buf bytes.Buffer
store := NewSecretStore()
store.Add("secretval")
inner := slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})
handler := NewSecretMaskingHandler(inner, store)
child := handler.WithAttrs([]slog.Attr{slog.String("static", "has secretval inside")})
log := slog.New(child)
log.Debug("test")
output := buf.String()
if strings.Contains(output, "secretval") {
t.Errorf("pre-set attr should be masked: %s", output)
}
}