// Package logging provides request logging functionality for the CLI Proxy API server. // This file contains fuzz tests for the StreamingSanitizer functionality. package logging import ( "bytes" "strings" "testing" ) // FuzzSanitizeBytes tests the sanitizeBytes function with various inputs // to ensure it correctly redacts sensitive fields without corrupting data. func FuzzSanitizeBytes(f *testing.F) { // Seed corpus with various input patterns f.Add([]byte(`{"thinking": "some secret thought"}`)) f.Add([]byte(`{"arguments": "secret args"}`)) f.Add([]byte(`{"thinking": "thought", "arguments": "args"}`)) f.Add([]byte(`{"message": {"thinking": "nested thought"}}`)) f.Add([]byte(`{}`)) f.Add([]byte(`[]`)) f.Add([]byte(``)) f.Add([]byte(`{"thinking": ""}`)) f.Add([]byte(`{"thinking": "multi\nline\ncontent"}`)) f.Add([]byte(`{"thinking": "escaped \"quotes\" here"}`)) f.Add([]byte(`{"other": "thinking: not redacted"}`)) f.Add([]byte(`{"thinking": "value", "other": "data", "arguments": "more"}`)) f.Fuzz(func(t *testing.T, data []byte) { result := sanitizeBytes(data) // Ensure result doesn't contain actual thinking values resultStr := string(result) // Check that "thinking" values are redacted // The regex matches: "thinking": "..." if strings.Contains(resultStr, `"thinking"`) { // If thinking key exists, its value should be [REDACTED] // This is a basic check - the actual value should be replaced if strings.Contains(resultStr, `"thinking": "`) && !strings.Contains(resultStr, `"thinking": "[REDACTED]"`) { // Check if it's not just the key with empty value or special patterns // Extract the value after "thinking": idx := strings.Index(resultStr, `"thinking"`) if idx >= 0 { afterKey := resultStr[idx+len(`"thinking"`):] afterKey = strings.TrimLeft(afterKey, " \t\n\r") if strings.HasPrefix(afterKey, `:`) { afterColon := strings.TrimLeft(afterKey[1:], " \t\n\r") if strings.HasPrefix(afterColon, `"`) { // It's a string value, check if properly redacted endQuote := strings.Index(afterColon[1:], `"`) if endQuote > 0 { value := afterColon[1 : endQuote+1] if value != "[REDACTED]" && value != "" { t.Errorf("thinking value not redacted: got %q", value) } } } } } } } // Check that "arguments" values are redacted if strings.Contains(resultStr, `"arguments"`) { if strings.Contains(resultStr, `"arguments": "`) && !strings.Contains(resultStr, `"arguments": "[REDACTED]"`) { idx := strings.Index(resultStr, `"arguments"`) if idx >= 0 { afterKey := resultStr[idx+len(`"arguments"`):] afterKey = strings.TrimLeft(afterKey, " \t\n\r") if strings.HasPrefix(afterKey, `:`) { afterColon := strings.TrimLeft(afterKey[1:], " \t\n\r") if strings.HasPrefix(afterColon, `"`) { endQuote := strings.Index(afterColon[1:], `"`) if endQuote > 0 { value := afterColon[1 : endQuote+1] if value != "[REDACTED]" && value != "" { t.Errorf("arguments value not redacted: got %q", value) } } } } } } } // Ensure the result doesn't contain the original input's sensitive values // (unless they happen to be "[REDACTED]") inputStr := string(data) if strings.Contains(inputStr, `"thinking"`) { // Extract thinking value from input thinkingVal := extractJSONStringValue(inputStr, "thinking") if thinkingVal != "" && thinkingVal != "[REDACTED]" { if strings.Contains(resultStr, thinkingVal) && !strings.Contains(dataStrWithoutKey(resultStr, "thinking"), thinkingVal) { t.Errorf("original thinking value %q still present in output", thinkingVal) } } } if strings.Contains(inputStr, `"arguments"`) { argsVal := extractJSONStringValue(inputStr, "arguments") if argsVal != "" && argsVal != "[REDACTED]" { if strings.Contains(resultStr, argsVal) && !strings.Contains(dataStrWithoutKey(resultStr, "arguments"), argsVal) { t.Errorf("original arguments value %q still present in output", argsVal) } } } // Ensure result is valid (doesn't panic, is non-nil) if result == nil && len(data) > 0 { t.Error("sanitizeBytes returned nil for non-empty input") } }) } // FuzzSanitizeBytesStructured specifically tests structured JSON inputs func FuzzSanitizeBytesStructured(f *testing.F) { // Valid JSON with thinking field f.Add([]byte(`{ "model": "gpt-4", "thinking": "This is my internal reasoning process", "messages": [{"role": "user", "content": "Hello"}] }`)) // Valid JSON with arguments field f.Add([]byte(`{ "function": "calculate", "arguments": "{\"x\": 10, \"y\": 20}", "other": "data" }`)) // Both fields f.Add([]byte(`{ "thinking": "secret thought", "arguments": "secret args", "output": "public result" }`)) // Nested thinking (should not be matched by current regex) f.Add([]byte(`{"nested": {"thinking": "nested value"}}`)) // Array with thinking objects f.Add([]byte(`[{"thinking": "first"}, {"thinking": "second"}]`)) f.Fuzz(func(t *testing.T, data []byte) { result := sanitizeBytes(data) // Basic sanity checks if len(result) > len(data)*2 { t.Logf("Result significantly larger than input: input=%d, output=%d", len(data), len(result)) } // Ensure no panics occurred (we got here) _ = result }) } // FuzzSanitizeBytesEdgeCases tests edge cases and boundary conditions func FuzzSanitizeBytesEdgeCases(f *testing.F) { // Empty input // Note: empty input returns nil from regex ReplaceAll, which is valid behavior // f.Add([]byte{}) // Single character f.Add([]byte(`{`)) // Just the key names without proper JSON structure f.Add([]byte(`thinking`)) f.Add([]byte(`arguments`)) // Keys with various whitespace patterns f.Add([]byte(`{"thinking" : "value"}`)) f.Add([]byte(`{"thinking":"value"}`)) f.Add([]byte(`{"thinking" : "value"}`)) // Escaped quotes in values f.Add([]byte(`{"thinking": "value with \"quotes\""}`)) // Unicode content f.Add([]byte(`{"thinking": "日本語テキスト"}`)) f.Add([]byte(`{"thinking": "🎉 emoji test"}`)) // Very long value longValue := bytes.Repeat([]byte("a"), 10000) f.Add(append([]byte(`{"thinking": "`), append(longValue, []byte(`"}`)...)...)) // Binary-like content f.Add([]byte{0x00, 0x01, 0x02, 0x03}) f.Fuzz(func(t *testing.T, data []byte) { // Should not panic result := sanitizeBytes(data) // Result may be nil for empty input (valid behavior from regex ReplaceAll) // Result should be valid bytes when non-nil if result != nil { _ = len(result) } }) } // Helper function to extract string value from JSON-like structure func extractJSONStringValue(jsonStr, key string) string { keyPattern := `"` + key + `"` idx := strings.Index(jsonStr, keyPattern) if idx < 0 { return "" } afterKey := jsonStr[idx+len(keyPattern):] afterKey = strings.TrimLeft(afterKey, " \t\n\r") if !strings.HasPrefix(afterKey, `:`) { return "" } afterColon := strings.TrimLeft(afterKey[1:], " \t\n\r") if !strings.HasPrefix(afterColon, `"`) { return "" } // Find closing quote (not escaped) start := 1 for i := start; i < len(afterColon); i++ { if afterColon[i] == '"' && (i == 0 || afterColon[i-1] != '\\') { return afterColon[start:i] } } return "" } // Helper function to get string without a specific key's value func dataStrWithoutKey(dataStr, key string) string { keyPattern := `"` + key + `"` idx := strings.Index(dataStr, keyPattern) if idx < 0 { return dataStr } // Return everything before the key return dataStr[:idx] } // TestSanitizeBytesSpecificCases tests specific known patterns func TestSanitizeBytesSpecificCases(t *testing.T) { tests := []struct { name string input string expected string }{ { name: "simple thinking", input: `{"thinking": "secret"}`, expected: `{"thinking": "[REDACTED]"}`, }, { name: "simple arguments", input: `{"arguments": "secret"}`, expected: `{"arguments": "[REDACTED]"}`, }, { name: "both fields", input: `{"thinking": "thought", "arguments": "args"}`, expected: `{"thinking": "[REDACTED]", "arguments": "[REDACTED]"}`, }, { name: "with whitespace", input: `{"thinking" : "secret"}`, expected: `{"thinking": "[REDACTED]"}`, }, { name: "escaped quotes", input: `{"thinking": "has \"quotes\""}`, expected: `{"thinking": "[REDACTED]"}`, }, { name: "empty value", input: `{"thinking": ""}`, expected: `{"thinking": "[REDACTED]"}`, }, { name: "no match", input: `{"other": "thinking: not redacted"}`, expected: `{"other": "thinking: not redacted"}`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := sanitizeBytes([]byte(tt.input)) if string(result) != tt.expected { t.Errorf("sanitizeBytes() = %q, want %q", string(result), tt.expected) } }) } } // TestSanitizeBytesPreservesStructure ensures sanitization doesn't break JSON structure func TestSanitizeBytesPreservesStructure(t *testing.T) { inputs := []string{ `{"model": "gpt-4", "thinking": "reasoning", "messages": []}`, `[{"thinking": "first"}, {"thinking": "second"}]`, `{"nested": {"thinking": "value"}}`, // Note: current regex doesn't match nested `{"thinking": "a", "other": "b", "arguments": "c"}`, } for _, input := range inputs { result := sanitizeBytes([]byte(input)) // Basic structural checks resultStr := string(result) // Count braces should match (basic JSON validity) openBraces := strings.Count(resultStr, "{") closeBraces := strings.Count(resultStr, "}") if openBraces != closeBraces { t.Errorf("Mismatched braces in: %s", resultStr) } // Count brackets should match openBrackets := strings.Count(resultStr, "[") closeBrackets := strings.Count(resultStr, "]") if openBrackets != closeBrackets { t.Errorf("Mismatched brackets in: %s", resultStr) } // Quotes should be even quotes := strings.Count(resultStr, `"`) if quotes%2 != 0 { t.Errorf("Unmatched quotes in: %s", resultStr) } } }