Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion internal/logger/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,32 @@ func parseLevel(level string) slog.Level {
}
}

var sensitiveKeys = map[string]bool{
// Authentication & API
"password": true,
"token": true,
"secret": true,
"api_key": true,
"auth_token": true,

// Connection details
"uri": true,
"address": true,
"host": true,
"port": true,
"bolt_uri": true,
}

// IsSensitiveKey checks if a key contains sensitive information that should be redacted.
func IsSensitiveKey(key string) bool {
_, exists := sensitiveKeys[strings.ToLower(key)]
return exists
}

// replaceAttr is a slog.HandlerOptions.ReplaceAttr function that customizes
// log level attribute formatting. It maps log levels to uppercase string
// representations using range-based switch cases (following slog custom levels pattern).
// All other attributes are passed through unchanged.
// It also redacts sensitive information from log attributes based on predefined keys.
func replaceAttr(_ []string, a slog.Attr) slog.Attr {
if a.Key == slog.LevelKey {
level := a.Value.Any().(slog.Level)
Expand All @@ -150,5 +172,11 @@ func replaceAttr(_ []string, a slog.Attr) slog.Attr {
a.Value = slog.StringValue("EMERGENCY")
}
}

// Redact sensitive information
if IsSensitiveKey(a.Key) {
a.Value = slog.StringValue("[REDACTED]")
}

return a
}
188 changes: 188 additions & 0 deletions internal/logger/logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,191 @@ func TestDynamicLogLevelChange(t *testing.T) {
}
})
}

func TestRedactionLogic(t *testing.T) {
t.Run("sensitive keys are redacted", func(t *testing.T) {
sensitiveFields := map[string]string{
"password": "my-secret-password",
"token": "bearer-token-123",
"api_key": "sk-1234567890",
"secret": "super-secret-value",
"auth_token": "auth-token-xyz",
"uri": "bolt://user:pass@localhost:7687",
"address": "192.168.1.1",
"host": "localhost",
"port": "7687",
"bolt_uri": "bolt://localhost:7687",
}

for key, sensitiveValue := range sensitiveFields {
buf := &bytes.Buffer{}
log := logger.New("info", "text", buf)

log.Info("test message", key, sensitiveValue)
output := buf.String()

if strings.Contains(output, sensitiveValue) {
t.Errorf("Expected %q to be redacted, but found value in output: %s", key, output)
}
if !strings.Contains(output, "[REDACTED]") {
t.Errorf("Expected [REDACTED] marker for %q in output: %s", key, output)
}
}
})

t.Run("sensitive keys are redacted in JSON format", func(t *testing.T) {
buf := &bytes.Buffer{}
log := logger.New("info", "json", buf)

log.Info("connection attempt",
"password", "secret123",
"token", "abc-def-ghi",
"host", "db.example.com",
"api_key", "key-xyz")

output := buf.String()
var logEntry map[string]any
if err := json.Unmarshal([]byte(output), &logEntry); err != nil {
t.Fatalf("Expected valid JSON output, got error: %v", err)
}

// Check that all sensitive fields are redacted
if password, exists := logEntry["password"]; exists && password != "[REDACTED]" {
t.Errorf("Expected password to be [REDACTED], got: %v", password)
}
if token, exists := logEntry["token"]; exists && token != "[REDACTED]" {
t.Errorf("Expected token to be [REDACTED], got: %v", token)
}
if host, exists := logEntry["host"]; exists && host != "[REDACTED]" {
t.Errorf("Expected host to be [REDACTED], got: %v", host)
}
if apiKey, exists := logEntry["api_key"]; exists && apiKey != "[REDACTED]" {
t.Errorf("Expected api_key to be [REDACTED], got: %v", apiKey)
}
})

t.Run("non-sensitive keys are not redacted", func(t *testing.T) {
buf := &bytes.Buffer{}
log := logger.New("info", "text", buf)

log.Info("user action",
"user_id", "12345",
"action", "login",
"timestamp", "2024-01-01T00:00:00Z",
"region", "us-east-1")

output := buf.String()

// Non-sensitive values should appear in output
if !strings.Contains(output, "12345") {
t.Error("Expected non-sensitive value user_id to appear in output")
}
if !strings.Contains(output, "login") {
t.Error("Expected non-sensitive value action to appear in output")
}
if !strings.Contains(output, "us-east-1") {
t.Error("Expected non-sensitive value region to appear in output")
}
})

t.Run("case-insensitive redaction for sensitive keys", func(t *testing.T) {
caseVariations := []string{
"PASSWORD",
"Password",
"PaSsWoRd",
"TOKEN",
"Token",
"API_KEY",
"Api_Key",
}

for _, keyVariation := range caseVariations {
buf := &bytes.Buffer{}
log := logger.New("info", "text", buf)

log.Info("test", keyVariation, "sensitive-value")
output := buf.String()

if strings.Contains(output, "sensitive-value") {
t.Errorf("Expected %q (case variation) to be redacted, but found value in output: %s", keyVariation, output)
}
if !strings.Contains(output, "[REDACTED]") {
t.Errorf("Expected [REDACTED] marker for %q in output: %s", keyVariation, output)
}
}
})

t.Run("mixed sensitive and non-sensitive fields", func(t *testing.T) {
buf := &bytes.Buffer{}
log := logger.New("info", "json", buf)

log.Info("database connection",
"host", "localhost",
"port", "7687",
"database", "neo4j",
"username", "neo4j",
"password", "secret123")

output := buf.String()
var logEntry map[string]any
if err := json.Unmarshal([]byte(output), &logEntry); err != nil {
t.Fatalf("Expected valid JSON output, got error: %v", err)
}

// Sensitive fields should be redacted
if password, exists := logEntry["password"]; !exists || password != "[REDACTED]" {
t.Errorf("Expected password to be [REDACTED], got: %v", password)
}

// Non-sensitive fields should not be redacted
if database, exists := logEntry["database"]; !exists || database != "neo4j" {
t.Errorf("Expected database to be 'neo4j', got: %v", database)
}
if portVal, exists := logEntry["port"]; !exists || portVal != "[REDACTED]" {
t.Errorf("Expected port to be [REDACTED] (sensitive field), got: %v", portVal)
}
})

t.Run("isSensitiveKey function works correctly", func(t *testing.T) {
testCases := []struct {
key string
shouldMask bool
}{
// Sensitive keys - Authentication & API
{"password", true},
{"Password", true},
{"PASSWORD", true},
{"token", true},
{"api_key", true},
{"secret", true},
{"auth_token", true},

// Sensitive keys - Connection details
{"uri", true},
{"address", true},
{"host", true},
{"port", true},
{"bolt_uri", true},

// Non-sensitive keys
{"user_id", false},
{"action", false},
{"timestamp", false},
{"region", false},
{"database", false},
{"username", false},
{"msg", false},
{"level", false},
{"server_address", false},
{"path", false},
{"certificate", false},
}

for _, tc := range testCases {
result := logger.IsSensitiveKey(tc.key)
if result != tc.shouldMask {
t.Errorf("isSensitiveKey(%q) = %v, expected %v", tc.key, result, tc.shouldMask)
}
}
})
}