8cf05a1576
Add a secret-masking slog.Handler that automatically replaces registered passwords with "***" in all log output. Secrets are registered per-scan when a discovery request arrives and unregistered when it completes. This approach masks credentials everywhere they appear in logs — URL userinfo, query parameters, path segments, and Go HTTP error messages — without modifying any business logic in scanner, builder, tester, or ONVIF components. API responses are unaffected and still return full URLs with credentials for frontend use.
204 lines
5.4 KiB
Go
204 lines
5.4 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_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)
|
|
}
|
|
}
|