3acc966658
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.
263 lines
7.7 KiB
Go
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)
|
|
}
|
|
}
|