From 5d0f9ba13461965b072336f537110eb162c65bcc Mon Sep 17 00:00:00 2001 From: Kerem Gocen Date: Mon, 17 Nov 2025 13:55:43 +0000 Subject: [PATCH 1/4] add redaction logic --- internal/logger/logger.go | 41 +++++++ internal/logger/logger_test.go | 199 +++++++++++++++++++++++++++++++++ 2 files changed, 240 insertions(+) diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 153d2ef..4ff43b0 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -76,6 +76,41 @@ func parseLevel(level string) slog.Level { } } +var sensitiveKeys = map[string]bool{ + // Authentication + "password": true, + "auth_token": true, + "token": true, + "secret": true, + "api_key": true, + + // Connection details + "uri": true, + "address": true, + "server_address": true, + "host": true, + "port": true, + "bolt_uri": true, + + // Encryption + "encryption_key": true, + "tls_key": true, + "certificate": true, + "ca_cert": true, + "ssl_cert": true, + + // System paths (infrastructure info) + "path": true, + "directory": true, + "backup_location": 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 +} + func replaceAttr(_ []string, a slog.Attr) slog.Attr { if a.Key == slog.LevelKey { level := a.Value.Any().(slog.Level) @@ -102,5 +137,11 @@ func replaceAttr(_ []string, a slog.Attr) slog.Attr { a.Value = slog.StringValue(levelName) } } + + // Redact sensitive information + if IsSensitiveKey(a.Key) { + a.Value = slog.StringValue("[REDACTED]") + } + return a } diff --git a/internal/logger/logger_test.go b/internal/logger/logger_test.go index bee1372..536ce17 100644 --- a/internal/logger/logger_test.go +++ b/internal/logger/logger_test.go @@ -161,3 +161,202 @@ 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", + "encryption_key": "key-encryption-123", + "tls_key": "tls-key-data", + "certificate": "cert-data", + "ca_cert": "ca-cert-data", + "ssl_cert": "ssl-cert-data", + "uri": "bolt://user:pass@localhost:7687", + "address": "192.168.1.1", + "server_address": "server.example.com", + "host": "localhost", + "bolt_uri": "bolt://localhost:7687", + "path": "/sensitive/path", + "directory": "/var/lib/sensitive", + "backup_location": "/backup/location", + } + + 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 + {"password", true}, + {"Password", true}, + {"PASSWORD", true}, + {"token", true}, + {"api_key", true}, + {"secret", true}, + {"auth_token", true}, + {"encryption_key", true}, + {"tls_key", true}, + {"certificate", true}, + {"ca_cert", true}, + {"ssl_cert", true}, + {"uri", true}, + {"address", true}, + {"server_address", true}, + {"host", true}, + {"bolt_uri", true}, + {"path", true}, + {"directory", true}, + {"backup_location", true}, + + // Non-sensitive keys + {"user_id", false}, + {"action", false}, + {"timestamp", false}, + {"region", false}, + {"database", false}, + {"username", false}, + {"msg", false}, + {"level", 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) + } + } + }) +} From 8bb3ebb9e66b27e46c62ba25d8a9f0db36b5235a Mon Sep 17 00:00:00 2001 From: Kerem Gocen Date: Mon, 17 Nov 2025 14:09:32 +0000 Subject: [PATCH 2/4] add integration tests writh real connection --- test/integration/logger/redaction_test.go | 203 ++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 test/integration/logger/redaction_test.go diff --git a/test/integration/logger/redaction_test.go b/test/integration/logger/redaction_test.go new file mode 100644 index 0000000..3691a02 --- /dev/null +++ b/test/integration/logger/redaction_test.go @@ -0,0 +1,203 @@ +//go:build integration + +package logger_test + +import ( + "bytes" + "context" + "encoding/json" + "os" + "strings" + "testing" + + "github.com/neo4j/mcp/internal/logger" + "github.com/neo4j/neo4j-go-driver/v5/neo4j" +) + +// TestRedactionWithNeo4jConnection tests redaction with real Neo4j connection scenarios +func TestRedactionWithNeo4jConnection(t *testing.T) { + // Get Neo4j connection details from environment + uri := os.Getenv("NEO4J_URI") + username := os.Getenv("NEO4J_USERNAME") + password := os.Getenv("NEO4J_PASSWORD") + database := os.Getenv("NEO4J_DATABASE") + + if uri == "" || username == "" || password == "" { + t.Skip("Neo4j environment variables not set. Set NEO4J_URI, NEO4J_USERNAME, NEO4J_PASSWORD to run integration tests") + } + + t.Run("logs with real Neo4j credentials are redacted", func(t *testing.T) { + buf := &bytes.Buffer{} + log := logger.New("debug", "json", buf) + + // Log actual connection parameters + log.Info("connecting to Neo4j", + "uri", uri, + "username", username, + "password", password, + "database", database, + "timeout", "30s") + + 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 uriVal, exists := logEntry["uri"]; exists && uriVal != "[REDACTED]" { + t.Errorf("Expected uri to be [REDACTED], but found actual value: %v", uriVal) + } + if pwdVal, exists := logEntry["password"]; exists && pwdVal != "[REDACTED]" { + t.Errorf("Expected password to be [REDACTED], but found actual value: %v", pwdVal) + } + + // Non-sensitive fields should not be redacted + if dbVal, exists := logEntry["database"]; !exists || dbVal != database { + t.Errorf("Expected database to be %q, got: %v", database, dbVal) + } + if timeoutVal, exists := logEntry["timeout"]; !exists || timeoutVal != "30s" { + t.Errorf("Expected timeout to be '30s', got: %v", timeoutVal) + } + }) + + t.Run("Neo4j connection errors don't leak credentials", func(t *testing.T) { + buf := &bytes.Buffer{} + log := logger.New("debug", "text", buf) + + // Test 1: Log real credentials with error + log.Error("neo4j connection failed", + "uri", uri, + "username", username, + "password", password, + "error", "authentication failed") + + output := buf.String() + + // Verify sensitive values don't appear as unredacted values in logs + // Check that password value is followed by [REDACTED] not the actual password + if !strings.Contains(output, "password=[REDACTED]") { + t.Error("Expected password to be [REDACTED] in error log") + } + + // For URI, check it's redacted (URI might be complex, so check for pattern) + if !strings.Contains(output, "uri=[REDACTED]") { + t.Error("Expected uri to be [REDACTED] in error log") + } + + // But the error message should still be there + if !strings.Contains(output, "authentication failed") { + t.Error("Expected error message to be in logs") + } + + // Test 2: Log different password attempt + buf.Reset() + wrongPassword := "wrong-password-12345" + log.Error("authentication retry", + "password", wrongPassword, + "attempt", "2") + + output = buf.String() + // Check that the specific password value is not in the output + // It should be [REDACTED] instead + if strings.Contains(output, wrongPassword) { + t.Error("Wrong password attempt was leaked in error log") + } + if !strings.Contains(output, "password=[REDACTED]") { + t.Error("Expected password to be [REDACTED] for wrong password attempt") + } + }) + + t.Run("actual Neo4j driver connection logs redaction", func(t *testing.T) { + buf := &bytes.Buffer{} + log := logger.New("debug", "json", buf) + + // Create actual Neo4j driver and log its initialization + driver, err := neo4j.NewDriverWithContext(uri, neo4j.BasicAuth(username, password, "")) + if err != nil { + // Even if connection fails, test that we log it safely + log.Error("failed to create neo4j driver", + "uri", uri, + "username", username, + "error", err.Error()) + + 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) + } + + // Verify credentials are redacted + if uriVal, exists := logEntry["uri"]; exists && uriVal != "[REDACTED]" { + t.Errorf("Expected uri to be [REDACTED] in error log, got: %v", uriVal) + } + return + } + defer driver.Close(context.Background()) + + // Connection succeeded, log it + log.Info("neo4j driver created successfully", + "uri", uri, + "username", username, + "database", database) + + 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) + } + + // Verify uri is redacted + if uriVal, exists := logEntry["uri"]; exists && uriVal != "[REDACTED]" { + t.Errorf("Expected uri to be [REDACTED] in success log, got: %v", uriVal) + } + }) + + t.Run("multiple connection attempts with various sensitive fields", func(t *testing.T) { + buf := &bytes.Buffer{} + log := logger.New("info", "json", buf) + + // Simulate multiple connection attempts with different sensitive fields + attempts := []map[string]string{ + { + "attempt": "1", + "uri": "bolt://localhost:7687", + "host": "localhost", + "port": "7687", + "token": "bearer-token-xyz", + }, + { + "attempt": "2", + "address": "192.168.1.100:7687", + "api_key": "sk-secret-key-123", + "secret": "some-secret-value", + "auth_token": "auth-xyz-789", + }, + { + "attempt": "3", + "bolt_uri": uri, + "encryption_key": "encryption-key-data", + "certificate": "cert-data-here", + "tls_key": "tls-key-data", + }, + } + + for _, attempt := range attempts { + buf.Reset() + log.Info("connection attempt", "attempt", attempt["attempt"]) + for key, value := range attempt { + if key != "attempt" { + log.Info("connection parameter", key, value) + } + } + + output := buf.String() + // Verify no actual sensitive values appear in any log line + for key, value := range attempt { + if strings.Contains(output, value) && key != "attempt" { + t.Errorf("Sensitive value for key %q was not redacted: %s", key, value) + } + } + } + }) +} From 8a9636945e750a5088a32f1853bfecc0d8939861 Mon Sep 17 00:00:00 2001 From: Kerem Gocen Date: Mon, 17 Nov 2025 14:14:25 +0000 Subject: [PATCH 3/4] use a shorter list, we can add more later --- internal/logger/logger.go | 35 ++++++------------ internal/logger/logger_test.go | 45 +++++++++-------------- test/integration/logger/redaction_test.go | 18 ++++----- 3 files changed, 36 insertions(+), 62 deletions(-) diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 4ff43b0..b87adf8 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -77,32 +77,19 @@ func parseLevel(level string) slog.Level { } var sensitiveKeys = map[string]bool{ - // Authentication - "password": true, - "auth_token": true, - "token": true, - "secret": true, - "api_key": true, + // Authentication & API + "password": true, + "token": true, + "secret": true, + "api_key": true, + "auth_token": true, // Connection details - "uri": true, - "address": true, - "server_address": true, - "host": true, - "port": true, - "bolt_uri": true, - - // Encryption - "encryption_key": true, - "tls_key": true, - "certificate": true, - "ca_cert": true, - "ssl_cert": true, - - // System paths (infrastructure info) - "path": true, - "directory": true, - "backup_location": true, + "uri": true, + "address": true, + "host": true, + "port": true, + "bolt_uri": true, } // IsSensitiveKey checks if a key contains sensitive information that should be redacted. diff --git a/internal/logger/logger_test.go b/internal/logger/logger_test.go index 536ce17..fb4c5d8 100644 --- a/internal/logger/logger_test.go +++ b/internal/logger/logger_test.go @@ -165,24 +165,16 @@ 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", - "encryption_key": "key-encryption-123", - "tls_key": "tls-key-data", - "certificate": "cert-data", - "ca_cert": "ca-cert-data", - "ssl_cert": "ssl-cert-data", - "uri": "bolt://user:pass@localhost:7687", - "address": "192.168.1.1", - "server_address": "server.example.com", - "host": "localhost", - "bolt_uri": "bolt://localhost:7687", - "path": "/sensitive/path", - "directory": "/var/lib/sensitive", - "backup_location": "/backup/location", + "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 { @@ -319,7 +311,7 @@ func TestRedactionLogic(t *testing.T) { key string shouldMask bool }{ - // Sensitive keys + // Sensitive keys - Authentication & API {"password", true}, {"Password", true}, {"PASSWORD", true}, @@ -327,19 +319,13 @@ func TestRedactionLogic(t *testing.T) { {"api_key", true}, {"secret", true}, {"auth_token", true}, - {"encryption_key", true}, - {"tls_key", true}, - {"certificate", true}, - {"ca_cert", true}, - {"ssl_cert", true}, + + // Sensitive keys - Connection details {"uri", true}, {"address", true}, - {"server_address", true}, {"host", true}, + {"port", true}, {"bolt_uri", true}, - {"path", true}, - {"directory", true}, - {"backup_location", true}, // Non-sensitive keys {"user_id", false}, @@ -350,6 +336,9 @@ func TestRedactionLogic(t *testing.T) { {"username", false}, {"msg", false}, {"level", false}, + {"server_address", false}, + {"path", false}, + {"certificate", false}, } for _, tc := range testCases { diff --git a/test/integration/logger/redaction_test.go b/test/integration/logger/redaction_test.go index 3691a02..9945854 100644 --- a/test/integration/logger/redaction_test.go +++ b/test/integration/logger/redaction_test.go @@ -167,18 +167,16 @@ func TestRedactionWithNeo4jConnection(t *testing.T) { "token": "bearer-token-xyz", }, { - "attempt": "2", - "address": "192.168.1.100:7687", - "api_key": "sk-secret-key-123", - "secret": "some-secret-value", - "auth_token": "auth-xyz-789", + "attempt": "2", + "address": "192.168.1.100:7687", + "api_key": "sk-secret-key-123", + "secret": "some-secret-value", + "auth_token": "auth-xyz-789", }, { - "attempt": "3", - "bolt_uri": uri, - "encryption_key": "encryption-key-data", - "certificate": "cert-data-here", - "tls_key": "tls-key-data", + "attempt": "3", + "bolt_uri": uri, + "password": "pwd-attempt-3", }, } From cb6dff5cb379ac1da35a600bd2cfba02716a9e83 Mon Sep 17 00:00:00 2001 From: Kerem Gocen Date: Thu, 20 Nov 2025 10:32:45 +0000 Subject: [PATCH 4/4] remove integration tests that rely on env vars --- test/integration/logger/redaction_test.go | 201 ---------------------- 1 file changed, 201 deletions(-) delete mode 100644 test/integration/logger/redaction_test.go diff --git a/test/integration/logger/redaction_test.go b/test/integration/logger/redaction_test.go deleted file mode 100644 index 9945854..0000000 --- a/test/integration/logger/redaction_test.go +++ /dev/null @@ -1,201 +0,0 @@ -//go:build integration - -package logger_test - -import ( - "bytes" - "context" - "encoding/json" - "os" - "strings" - "testing" - - "github.com/neo4j/mcp/internal/logger" - "github.com/neo4j/neo4j-go-driver/v5/neo4j" -) - -// TestRedactionWithNeo4jConnection tests redaction with real Neo4j connection scenarios -func TestRedactionWithNeo4jConnection(t *testing.T) { - // Get Neo4j connection details from environment - uri := os.Getenv("NEO4J_URI") - username := os.Getenv("NEO4J_USERNAME") - password := os.Getenv("NEO4J_PASSWORD") - database := os.Getenv("NEO4J_DATABASE") - - if uri == "" || username == "" || password == "" { - t.Skip("Neo4j environment variables not set. Set NEO4J_URI, NEO4J_USERNAME, NEO4J_PASSWORD to run integration tests") - } - - t.Run("logs with real Neo4j credentials are redacted", func(t *testing.T) { - buf := &bytes.Buffer{} - log := logger.New("debug", "json", buf) - - // Log actual connection parameters - log.Info("connecting to Neo4j", - "uri", uri, - "username", username, - "password", password, - "database", database, - "timeout", "30s") - - 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 uriVal, exists := logEntry["uri"]; exists && uriVal != "[REDACTED]" { - t.Errorf("Expected uri to be [REDACTED], but found actual value: %v", uriVal) - } - if pwdVal, exists := logEntry["password"]; exists && pwdVal != "[REDACTED]" { - t.Errorf("Expected password to be [REDACTED], but found actual value: %v", pwdVal) - } - - // Non-sensitive fields should not be redacted - if dbVal, exists := logEntry["database"]; !exists || dbVal != database { - t.Errorf("Expected database to be %q, got: %v", database, dbVal) - } - if timeoutVal, exists := logEntry["timeout"]; !exists || timeoutVal != "30s" { - t.Errorf("Expected timeout to be '30s', got: %v", timeoutVal) - } - }) - - t.Run("Neo4j connection errors don't leak credentials", func(t *testing.T) { - buf := &bytes.Buffer{} - log := logger.New("debug", "text", buf) - - // Test 1: Log real credentials with error - log.Error("neo4j connection failed", - "uri", uri, - "username", username, - "password", password, - "error", "authentication failed") - - output := buf.String() - - // Verify sensitive values don't appear as unredacted values in logs - // Check that password value is followed by [REDACTED] not the actual password - if !strings.Contains(output, "password=[REDACTED]") { - t.Error("Expected password to be [REDACTED] in error log") - } - - // For URI, check it's redacted (URI might be complex, so check for pattern) - if !strings.Contains(output, "uri=[REDACTED]") { - t.Error("Expected uri to be [REDACTED] in error log") - } - - // But the error message should still be there - if !strings.Contains(output, "authentication failed") { - t.Error("Expected error message to be in logs") - } - - // Test 2: Log different password attempt - buf.Reset() - wrongPassword := "wrong-password-12345" - log.Error("authentication retry", - "password", wrongPassword, - "attempt", "2") - - output = buf.String() - // Check that the specific password value is not in the output - // It should be [REDACTED] instead - if strings.Contains(output, wrongPassword) { - t.Error("Wrong password attempt was leaked in error log") - } - if !strings.Contains(output, "password=[REDACTED]") { - t.Error("Expected password to be [REDACTED] for wrong password attempt") - } - }) - - t.Run("actual Neo4j driver connection logs redaction", func(t *testing.T) { - buf := &bytes.Buffer{} - log := logger.New("debug", "json", buf) - - // Create actual Neo4j driver and log its initialization - driver, err := neo4j.NewDriverWithContext(uri, neo4j.BasicAuth(username, password, "")) - if err != nil { - // Even if connection fails, test that we log it safely - log.Error("failed to create neo4j driver", - "uri", uri, - "username", username, - "error", err.Error()) - - 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) - } - - // Verify credentials are redacted - if uriVal, exists := logEntry["uri"]; exists && uriVal != "[REDACTED]" { - t.Errorf("Expected uri to be [REDACTED] in error log, got: %v", uriVal) - } - return - } - defer driver.Close(context.Background()) - - // Connection succeeded, log it - log.Info("neo4j driver created successfully", - "uri", uri, - "username", username, - "database", database) - - 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) - } - - // Verify uri is redacted - if uriVal, exists := logEntry["uri"]; exists && uriVal != "[REDACTED]" { - t.Errorf("Expected uri to be [REDACTED] in success log, got: %v", uriVal) - } - }) - - t.Run("multiple connection attempts with various sensitive fields", func(t *testing.T) { - buf := &bytes.Buffer{} - log := logger.New("info", "json", buf) - - // Simulate multiple connection attempts with different sensitive fields - attempts := []map[string]string{ - { - "attempt": "1", - "uri": "bolt://localhost:7687", - "host": "localhost", - "port": "7687", - "token": "bearer-token-xyz", - }, - { - "attempt": "2", - "address": "192.168.1.100:7687", - "api_key": "sk-secret-key-123", - "secret": "some-secret-value", - "auth_token": "auth-xyz-789", - }, - { - "attempt": "3", - "bolt_uri": uri, - "password": "pwd-attempt-3", - }, - } - - for _, attempt := range attempts { - buf.Reset() - log.Info("connection attempt", "attempt", attempt["attempt"]) - for key, value := range attempt { - if key != "attempt" { - log.Info("connection parameter", key, value) - } - } - - output := buf.String() - // Verify no actual sensitive values appear in any log line - for key, value := range attempt { - if strings.Contains(output, value) && key != "attempt" { - t.Errorf("Sensitive value for key %q was not redacted: %s", key, value) - } - } - } - }) -}