feat: create v2-prep branch with comprehensive planning
Restructured project for V2 refactor: **Structure Changes:** - Moved all V1 code to orig/ folder (preserved with git mv) - Created docs/planning/ directory - Added orig/README_V1.md explaining V1 preservation **Planning Documents:** - 00_V2_MASTER_PLAN.md: Complete architecture overview - Executive summary of critical V1 issues - High-level component architecture diagrams - 5-phase implementation roadmap - Success metrics and risk mitigation - 07_TASK_BREAKDOWN.md: Atomic task breakdown - 99+ hours of detailed tasks - Every task < 2 hours (atomic) - Clear dependencies and success criteria - Organized by implementation phase **V2 Key Improvements:** - Per-exchange parsers (factory pattern) - Multi-layer strict validation - Multi-index pool cache - Background validation pipeline - Comprehensive observability **Critical Issues Addressed:** - Zero address tokens (strict validation + cache enrichment) - Parsing accuracy (protocol-specific parsers) - No audit trail (background validation channel) - Inefficient lookups (multi-index cache) - Stats disconnection (event-driven metrics) Next Steps: 1. Review planning documents 2. Begin Phase 1: Foundation (P1-001 through P1-010) 3. Implement parsers in Phase 2 4. Build cache system in Phase 3 5. Add validation pipeline in Phase 4 6. Migrate and test in Phase 5 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
484
orig/internal/logger/logger.go
Normal file
484
orig/internal/logger/logger.go
Normal file
@@ -0,0 +1,484 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
pkgerrors "github.com/fraktal/mev-beta/pkg/errors"
|
||||
)
|
||||
|
||||
// LogLevel represents different log levels
|
||||
type LogLevel int
|
||||
|
||||
const (
|
||||
DEBUG LogLevel = iota
|
||||
INFO
|
||||
WARN
|
||||
ERROR
|
||||
OPPORTUNITY // Special level for opportunities
|
||||
)
|
||||
|
||||
var logLevelNames = map[LogLevel]string{
|
||||
DEBUG: "DEBUG",
|
||||
INFO: "INFO",
|
||||
WARN: "WARN",
|
||||
ERROR: "ERROR",
|
||||
OPPORTUNITY: "OPPORTUNITY",
|
||||
}
|
||||
|
||||
var suppressedWarningSubstrings = []string{
|
||||
"extractTokensGeneric",
|
||||
"extractTokensFromMulticall",
|
||||
}
|
||||
|
||||
// Logger represents a multi-file logger with separation of concerns
|
||||
type Logger struct {
|
||||
// Main application logger
|
||||
logger *log.Logger
|
||||
level LogLevel
|
||||
|
||||
// Specialized loggers for different concerns
|
||||
opportunityLogger *log.Logger // MEV opportunities and arbitrage attempts
|
||||
errorLogger *log.Logger // Errors and warnings only
|
||||
performanceLogger *log.Logger // Performance metrics and RPC calls
|
||||
transactionLogger *log.Logger // Detailed transaction analysis
|
||||
|
||||
// Security filtering
|
||||
secureFilter *SecureFilter
|
||||
|
||||
levelName string
|
||||
}
|
||||
|
||||
// parseLogLevel converts string log level to LogLevel enum
|
||||
func parseLogLevel(level string) LogLevel {
|
||||
switch strings.ToLower(level) {
|
||||
case "debug":
|
||||
return DEBUG
|
||||
case "info":
|
||||
return INFO
|
||||
case "warn", "warning":
|
||||
return WARN
|
||||
case "error":
|
||||
return ERROR
|
||||
default:
|
||||
return INFO // Default to INFO level
|
||||
}
|
||||
}
|
||||
|
||||
// createLogFile creates a log file or returns stdout if it fails
|
||||
func createLogFile(filename string) *os.File {
|
||||
if filename == "" {
|
||||
return os.Stdout
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(filename), 0o755); err != nil {
|
||||
log.Printf("Failed to create log directory for %s: %v, falling back to stdout", filename, err)
|
||||
return os.Stdout
|
||||
}
|
||||
|
||||
// Check and rotate log file if needed (100MB max size)
|
||||
maxSize := int64(100 * 1024 * 1024) // 100 MB
|
||||
if err := rotateLogFile(filename, maxSize); err != nil {
|
||||
log.Printf("Failed to rotate log file %s: %v", filename, err)
|
||||
// Continue anyway, rotation failure shouldn't stop logging
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create log file %s: %v, falling back to stdout", filename, err)
|
||||
return os.Stdout
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// New creates a new multi-file logger with separation of concerns
|
||||
func New(level string, format string, file string) *Logger {
|
||||
// Parse base filename for specialized logs
|
||||
baseDir := "logs"
|
||||
baseName := "mev_bot"
|
||||
if file != "" {
|
||||
// Extract directory and base filename
|
||||
parts := strings.Split(file, "/")
|
||||
if len(parts) > 1 {
|
||||
baseDir = strings.Join(parts[:len(parts)-1], "/")
|
||||
}
|
||||
filename := parts[len(parts)-1]
|
||||
if strings.Contains(filename, ".") {
|
||||
baseName = strings.Split(filename, ".")[0]
|
||||
}
|
||||
}
|
||||
|
||||
// Create specialized log files
|
||||
mainFile := createLogFile(file)
|
||||
opportunityFile := createLogFile(fmt.Sprintf("%s/%s_opportunities.log", baseDir, baseName))
|
||||
errorFile := createLogFile(fmt.Sprintf("%s/%s_errors.log", baseDir, baseName))
|
||||
performanceFile := createLogFile(fmt.Sprintf("%s/%s_performance.log", baseDir, baseName))
|
||||
transactionFile := createLogFile(fmt.Sprintf("%s/%s_transactions.log", baseDir, baseName))
|
||||
|
||||
// Create loggers with no prefixes (we format ourselves)
|
||||
logLevel := parseLogLevel(level)
|
||||
|
||||
// Determine security level based on environment and log level
|
||||
var securityLevel SecurityLevel
|
||||
env := os.Getenv("GO_ENV")
|
||||
switch {
|
||||
case env == "production":
|
||||
securityLevel = SecurityLevelProduction
|
||||
case logLevel >= WARN:
|
||||
securityLevel = SecurityLevelInfo
|
||||
default:
|
||||
securityLevel = SecurityLevelDebug
|
||||
}
|
||||
|
||||
return &Logger{
|
||||
logger: log.New(mainFile, "", 0),
|
||||
opportunityLogger: log.New(opportunityFile, "", 0),
|
||||
errorLogger: log.New(errorFile, "", 0),
|
||||
performanceLogger: log.New(performanceFile, "", 0),
|
||||
transactionLogger: log.New(transactionFile, "", 0),
|
||||
level: logLevel,
|
||||
secureFilter: NewSecureFilter(securityLevel),
|
||||
levelName: level,
|
||||
}
|
||||
}
|
||||
|
||||
// shouldLog determines if a message should be logged based on level
|
||||
func (l *Logger) shouldLog(level LogLevel) bool {
|
||||
return level >= l.level
|
||||
}
|
||||
|
||||
// formatMessage formats a log message with timestamp and level
|
||||
func (l *Logger) formatMessage(level LogLevel, v ...interface{}) string {
|
||||
timestamp := time.Now().Format("2006/01/02 15:04:05")
|
||||
levelName := logLevelNames[level]
|
||||
message := formatKVMessage(v...)
|
||||
return fmt.Sprintf("%s [%s] %s", timestamp, levelName, message)
|
||||
}
|
||||
|
||||
// formatKVMessage converts a variadic list of arguments into a structured log string.
|
||||
// It treats consecutive key/value pairs (string key followed by any value) specially
|
||||
// so that existing logger calls like logger.Error("msg", "key", value) render as
|
||||
// `msg key=value`.
|
||||
func formatKVMessage(args ...interface{}) string {
|
||||
if len(args) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
|
||||
// Always print the first argument verbatim to preserve legacy formatting.
|
||||
fmt.Fprintf(&b, "%v", args[0])
|
||||
|
||||
// Process subsequent arguments as key/value pairs where possible.
|
||||
for i := 1; i < len(args); i++ {
|
||||
key, ok := args[i].(string)
|
||||
if !ok || i == len(args)-1 {
|
||||
// Not a key/value pair, fall back to simple spacing.
|
||||
fmt.Fprintf(&b, " %v", args[i])
|
||||
continue
|
||||
}
|
||||
|
||||
value := args[i+1]
|
||||
fmt.Fprintf(&b, " %s=%v", key, value)
|
||||
i++
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// Debug logs a debug message
|
||||
func (l *Logger) Debug(v ...interface{}) {
|
||||
if l.shouldLog(DEBUG) {
|
||||
l.logger.Println(l.formatMessage(DEBUG, v...))
|
||||
}
|
||||
}
|
||||
|
||||
// Info logs an info message
|
||||
func (l *Logger) Info(v ...interface{}) {
|
||||
if l.shouldLog(INFO) {
|
||||
l.logger.Println(l.formatMessage(INFO, v...))
|
||||
}
|
||||
}
|
||||
|
||||
// Warn logs a warning message
|
||||
func (l *Logger) Warn(v ...interface{}) {
|
||||
if l.shouldLog(WARN) {
|
||||
message := l.formatMessage(WARN, v...)
|
||||
for _, substr := range suppressedWarningSubstrings {
|
||||
if strings.Contains(message, substr) {
|
||||
return
|
||||
}
|
||||
}
|
||||
l.logger.Println(message)
|
||||
l.errorLogger.Println(message) // Also log to error file
|
||||
}
|
||||
}
|
||||
|
||||
// Error logs an error message
|
||||
func (l *Logger) Error(v ...interface{}) {
|
||||
if l.shouldLog(ERROR) {
|
||||
message := l.formatMessage(ERROR, v...)
|
||||
l.logger.Println(message)
|
||||
l.errorLogger.Println(message) // Also log to error file
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorStructured logs a structured error with full context
|
||||
func (l *Logger) ErrorStructured(err *pkgerrors.StructuredError) {
|
||||
if !l.shouldLog(ERROR) {
|
||||
return
|
||||
}
|
||||
|
||||
// Log compact format to main log
|
||||
compactMsg := fmt.Sprintf("%s [%s] %s",
|
||||
time.Now().Format("2006/01/02 15:04:05"),
|
||||
"ERROR",
|
||||
err.FormatCompact())
|
||||
l.logger.Println(compactMsg)
|
||||
|
||||
// Log full detailed format to error log
|
||||
fullMsg := fmt.Sprintf("%s [%s] %s",
|
||||
time.Now().Format("2006/01/02 15:04:05"),
|
||||
"ERROR",
|
||||
err.FormatForLogging())
|
||||
l.errorLogger.Println(fullMsg)
|
||||
}
|
||||
|
||||
// WarnStructured logs a structured warning with full context
|
||||
func (l *Logger) WarnStructured(err *pkgerrors.StructuredError) {
|
||||
if !l.shouldLog(WARN) {
|
||||
return
|
||||
}
|
||||
|
||||
// Log compact format to main log
|
||||
compactMsg := fmt.Sprintf("%s [%s] %s",
|
||||
time.Now().Format("2006/01/02 15:04:05"),
|
||||
"WARN",
|
||||
err.FormatCompact())
|
||||
|
||||
// Check if warning should be suppressed
|
||||
for _, substr := range suppressedWarningSubstrings {
|
||||
if strings.Contains(compactMsg, substr) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
l.logger.Println(compactMsg)
|
||||
l.errorLogger.Println(compactMsg)
|
||||
}
|
||||
|
||||
// Opportunity logs a found opportunity with detailed information
|
||||
// This always logs regardless of level since opportunities are critical
|
||||
func (l *Logger) Opportunity(txHash, from, to, method, protocol string, amountIn, amountOut, minOut, profitUSD float64, additionalData map[string]interface{}) {
|
||||
timestamp := time.Now().Format("2006/01/02 15:04:05")
|
||||
|
||||
// Create sanitized additional data for production
|
||||
sanitizedData := l.secureFilter.SanitizeForProduction(additionalData)
|
||||
|
||||
message := fmt.Sprintf(`%s [OPPORTUNITY] 🎯 ARBITRAGE OPPORTUNITY DETECTED
|
||||
├── Transaction: %s
|
||||
├── From: %s → To: %s
|
||||
├── Method: %s (%s)
|
||||
├── Amount In: %.6f tokens
|
||||
├── Amount Out: %.6f tokens
|
||||
├── Min Out: %.6f tokens
|
||||
├── Estimated Profit: $%.2f USD
|
||||
└── Additional Data: %v`,
|
||||
timestamp, txHash, from, to, method, protocol,
|
||||
amountIn, amountOut, minOut, profitUSD, sanitizedData)
|
||||
|
||||
// Apply security filtering to the entire message
|
||||
filteredMessage := l.secureFilter.FilterMessage(message)
|
||||
|
||||
l.logger.Println(filteredMessage)
|
||||
l.opportunityLogger.Println(filteredMessage) // Dedicated opportunity log
|
||||
}
|
||||
|
||||
// OpportunitySimple logs a simple opportunity message (for backwards compatibility)
|
||||
func (l *Logger) OpportunitySimple(v ...interface{}) {
|
||||
timestamp := time.Now().Format("2006/01/02 15:04:05")
|
||||
message := fmt.Sprintf("%s [OPPORTUNITY] %s", timestamp, fmt.Sprint(v...))
|
||||
l.logger.Println(message)
|
||||
l.opportunityLogger.Println(message) // Dedicated opportunity log
|
||||
}
|
||||
|
||||
// Performance logs performance metrics for optimization analysis
|
||||
func (l *Logger) Performance(component, operation string, duration time.Duration, metadata map[string]interface{}) {
|
||||
timestamp := time.Now().Format("2006/01/02 15:04:05")
|
||||
|
||||
// Add standard performance fields
|
||||
data := map[string]interface{}{
|
||||
"component": component,
|
||||
"operation": operation,
|
||||
"duration_ms": duration.Milliseconds(),
|
||||
"duration_ns": duration.Nanoseconds(),
|
||||
"timestamp": timestamp,
|
||||
}
|
||||
|
||||
// Merge with provided metadata
|
||||
for k, v := range metadata {
|
||||
data[k] = v
|
||||
}
|
||||
|
||||
message := fmt.Sprintf(`%s [PERFORMANCE] 📊 %s.%s completed in %v - %v`,
|
||||
timestamp, component, operation, duration, data)
|
||||
|
||||
l.performanceLogger.Println(message) // Dedicated performance log only
|
||||
}
|
||||
|
||||
// Metrics logs business metrics for analysis
|
||||
func (l *Logger) Metrics(name string, value float64, unit string, tags map[string]string) {
|
||||
timestamp := time.Now().Format("2006/01/02 15:04:05")
|
||||
|
||||
message := fmt.Sprintf(`%s [METRICS] 📈 %s: %.6f %s %v`,
|
||||
timestamp, name, value, unit, tags)
|
||||
|
||||
l.performanceLogger.Println(message) // Metrics go to performance log
|
||||
}
|
||||
|
||||
// Transaction logs detailed transaction information for MEV analysis
|
||||
func (l *Logger) Transaction(txHash, from, to, method, protocol string, gasUsed, gasPrice uint64, value float64, success bool, metadata map[string]interface{}) {
|
||||
timestamp := time.Now().Format("2006/01/02 15:04:05")
|
||||
|
||||
status := "FAILED"
|
||||
if success {
|
||||
status = "SUCCESS"
|
||||
}
|
||||
|
||||
// Sanitize metadata for production
|
||||
sanitizedMetadata := l.secureFilter.SanitizeForProduction(metadata)
|
||||
|
||||
message := fmt.Sprintf(`%s [TRANSACTION] 💳 %s
|
||||
├── Hash: %s
|
||||
├── From: %s → To: %s
|
||||
├── Method: %s (%s)
|
||||
├── Gas Used: %d (Price: %d wei)
|
||||
├── Value: %.6f ETH
|
||||
├── Status: %s
|
||||
└── Metadata: %v`,
|
||||
timestamp, status, txHash, from, to, method, protocol,
|
||||
gasUsed, gasPrice, value, status, sanitizedMetadata)
|
||||
|
||||
// Apply security filtering to the entire message
|
||||
filteredMessage := l.secureFilter.FilterMessage(message)
|
||||
|
||||
l.transactionLogger.Println(filteredMessage) // Dedicated transaction log only
|
||||
}
|
||||
|
||||
// BlockProcessing logs block processing metrics for sequencer monitoring
|
||||
func (l *Logger) BlockProcessing(blockNumber uint64, txCount, dexTxCount int, processingTime time.Duration) {
|
||||
timestamp := time.Now().Format("2006/01/02 15:04:05")
|
||||
|
||||
message := fmt.Sprintf(`%s [BLOCK_PROCESSING] 🧱 Block %d: %d txs (%d DEX) processed in %v`,
|
||||
timestamp, blockNumber, txCount, dexTxCount, processingTime)
|
||||
|
||||
l.performanceLogger.Println(message) // Block processing metrics go to performance log
|
||||
}
|
||||
|
||||
// ArbitrageAnalysis logs arbitrage opportunity analysis results
|
||||
func (l *Logger) ArbitrageAnalysis(poolA, poolB, tokenPair string, priceA, priceB, priceDiff, estimatedProfit float64, feasible bool) {
|
||||
timestamp := time.Now().Format("2006/01/02 15:04:05")
|
||||
|
||||
status := "REJECTED"
|
||||
if feasible {
|
||||
status = "VIABLE"
|
||||
}
|
||||
|
||||
message := fmt.Sprintf(`%s [ARBITRAGE_ANALYSIS] 🔍 %s %s
|
||||
├── Pool A: %s (Price: %.6f)
|
||||
├── Pool B: %s (Price: %.6f)
|
||||
├── Price Difference: %.4f%%
|
||||
├── Estimated Profit: $%.2f
|
||||
└── Status: %s`,
|
||||
timestamp, status, tokenPair, poolA, priceA, poolB, priceB,
|
||||
priceDiff*100, estimatedProfit, status)
|
||||
|
||||
// Apply security filtering to protect sensitive pricing data
|
||||
filteredMessage := l.secureFilter.FilterMessage(message)
|
||||
|
||||
l.opportunityLogger.Println(filteredMessage) // Arbitrage analysis goes to opportunity log
|
||||
}
|
||||
|
||||
// RPC logs RPC call metrics for endpoint optimization
|
||||
func (l *Logger) RPC(endpoint, method string, duration time.Duration, success bool, errorMsg string) {
|
||||
timestamp := time.Now().Format("2006/01/02 15:04:05")
|
||||
|
||||
status := "SUCCESS"
|
||||
if !success {
|
||||
status = "FAILED"
|
||||
}
|
||||
|
||||
message := fmt.Sprintf(`%s [RPC] 🌐 %s %s.%s in %v`,
|
||||
timestamp, status, endpoint, method, duration)
|
||||
|
||||
if !success && errorMsg != "" {
|
||||
message += fmt.Sprintf(" - Error: %s", errorMsg)
|
||||
}
|
||||
|
||||
l.performanceLogger.Println(message) // RPC metrics go to performance log
|
||||
}
|
||||
|
||||
// SwapAnalysis logs swap event analysis with security filtering
|
||||
func (l *Logger) SwapAnalysis(tokenIn, tokenOut string, amountIn, amountOut float64, protocol, poolAddr string, metadata map[string]interface{}) {
|
||||
timestamp := time.Now().Format("2006/01/02 15:04:05")
|
||||
|
||||
// Sanitize metadata for production
|
||||
sanitizedMetadata := l.secureFilter.SanitizeForProduction(metadata)
|
||||
|
||||
message := fmt.Sprintf(`%s [SWAP_ANALYSIS] 🔄 %s → %s
|
||||
├── Amount In: %.6f %s
|
||||
├── Amount Out: %.6f %s
|
||||
├── Protocol: %s
|
||||
├── Pool: %s
|
||||
└── Metadata: %v`,
|
||||
timestamp, tokenIn, tokenOut, amountIn, tokenIn, amountOut, tokenOut,
|
||||
protocol, poolAddr, sanitizedMetadata)
|
||||
|
||||
// Apply security filtering to the entire message
|
||||
filteredMessage := l.secureFilter.FilterMessage(message)
|
||||
|
||||
l.transactionLogger.Println(filteredMessage) // Dedicated transaction log
|
||||
}
|
||||
|
||||
// rotateLogFile rotates a log file when it exceeds the maximum size
|
||||
func rotateLogFile(filename string, maxSize int64) error {
|
||||
// Check if file exists
|
||||
if _, err := os.Stat(filename); os.IsNotExist(err) {
|
||||
return nil // File doesn't exist, nothing to rotate
|
||||
}
|
||||
|
||||
// Get file info
|
||||
fileInfo, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get file info: %w", err)
|
||||
}
|
||||
|
||||
// Check if file exceeds max size
|
||||
if fileInfo.Size() < maxSize {
|
||||
return nil // File is within size limits
|
||||
}
|
||||
|
||||
// Create archive directory if it doesn't exist
|
||||
archiveDir := "logs/archived"
|
||||
if err := os.MkdirAll(archiveDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create archive directory: %w", err)
|
||||
}
|
||||
|
||||
// Generate archive filename with timestamp
|
||||
timestamp := time.Now().Format("20060102_150405")
|
||||
baseName := filepath.Base(filename)
|
||||
ext := filepath.Ext(baseName)
|
||||
name := strings.TrimSuffix(baseName, ext)
|
||||
archiveFilename := filepath.Join(archiveDir, fmt.Sprintf("%s_%s%s", name, timestamp, ext))
|
||||
|
||||
// Close current file handle and rename
|
||||
if err := os.Rename(filename, archiveFilename); err != nil {
|
||||
return fmt.Errorf("failed to rotate log file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
243
orig/internal/logger/logger_test.go
Normal file
243
orig/internal/logger/logger_test.go
Normal file
@@ -0,0 +1,243 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewLogger(t *testing.T) {
|
||||
// Test creating a logger with stdout
|
||||
logger := New("info", "text", "")
|
||||
assert.NotNil(t, logger)
|
||||
assert.NotNil(t, logger.logger)
|
||||
assert.Equal(t, "info", logger.levelName)
|
||||
}
|
||||
|
||||
func TestNewLoggerWithFile(t *testing.T) {
|
||||
// Create a temporary file for testing
|
||||
tmpFile, err := os.CreateTemp("", "logger_test_*.log")
|
||||
assert.NoError(t, err)
|
||||
defer os.Remove(tmpFile.Name())
|
||||
err = tmpFile.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test creating a logger with a file
|
||||
logger := New("info", "text", tmpFile.Name())
|
||||
assert.NotNil(t, logger)
|
||||
assert.Equal(t, "info", logger.levelName)
|
||||
}
|
||||
|
||||
func TestDebug(t *testing.T) {
|
||||
// Capture stdout
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
// Create logger with debug level
|
||||
logger := New("debug", "text", "")
|
||||
|
||||
// Log a debug message
|
||||
logger.Debug("test debug message")
|
||||
|
||||
// Restore stdout
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
|
||||
// Read the captured output
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
output := buf.String()
|
||||
|
||||
// Check that the log message contains the expected content with brackets
|
||||
assert.Contains(t, output, "[DEBUG] test debug message")
|
||||
}
|
||||
|
||||
func TestDebugWithInfoLevel(t *testing.T) {
|
||||
// Capture stdout
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
// Create logger with info level (should not log debug messages)
|
||||
logger := New("info", "text", "")
|
||||
|
||||
// Log a debug message
|
||||
logger.Debug("test debug message")
|
||||
|
||||
// Restore stdout
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
|
||||
// Read the captured output
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
output := buf.String()
|
||||
|
||||
// Verify the output does not contain the debug message
|
||||
assert.NotContains(t, output, "DEBUG:")
|
||||
assert.NotContains(t, output, "test debug message")
|
||||
}
|
||||
|
||||
func TestInfo(t *testing.T) {
|
||||
// Capture stdout
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
// Create logger with info level
|
||||
logger := New("info", "text", "")
|
||||
|
||||
// Log an info message
|
||||
logger.Info("test info message")
|
||||
|
||||
// Restore stdout
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
|
||||
// Read the captured output
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
output := buf.String()
|
||||
|
||||
// Verify the output contains the info message
|
||||
assert.Contains(t, output, "[INFO]")
|
||||
assert.Contains(t, output, "test info message")
|
||||
}
|
||||
|
||||
func TestInfoWithDebugLevel(t *testing.T) {
|
||||
// Capture stdout
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
// Create logger with debug level
|
||||
logger := New("debug", "text", "")
|
||||
|
||||
// Log an info message
|
||||
logger.Info("test info message")
|
||||
|
||||
// Restore stdout
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
|
||||
// Read the captured output
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
output := buf.String()
|
||||
|
||||
// Verify the output contains the info message
|
||||
assert.Contains(t, output, "[INFO]")
|
||||
assert.Contains(t, output, "test info message")
|
||||
}
|
||||
|
||||
func TestWarn(t *testing.T) {
|
||||
// Capture stdout
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
// Create logger with warn level
|
||||
logger := New("warn", "text", "")
|
||||
|
||||
// Log a warning message
|
||||
logger.Warn("test warn message")
|
||||
|
||||
// Restore stdout
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
|
||||
// Read the captured output
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
output := buf.String()
|
||||
|
||||
// Verify the output contains the warning message
|
||||
assert.Contains(t, output, "[WARN]")
|
||||
assert.Contains(t, output, "test warn message")
|
||||
}
|
||||
|
||||
func TestWarnWithInfoLevel(t *testing.T) {
|
||||
// Capture stdout
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
// Create logger with info level (should log warnings)
|
||||
logger := New("info", "text", "")
|
||||
|
||||
// Log a warning message
|
||||
logger.Warn("test warn message")
|
||||
|
||||
// Restore stdout
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
|
||||
// Read the captured output
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
output := buf.String()
|
||||
|
||||
// Verify the output contains the warning message
|
||||
assert.Contains(t, output, "[WARN]")
|
||||
assert.Contains(t, output, "test warn message")
|
||||
}
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
// Capture stdout
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
// Create logger
|
||||
logger := New("error", "text", "")
|
||||
|
||||
// Log an error message
|
||||
logger.Error("test error message")
|
||||
|
||||
// Restore stdout
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
|
||||
// Read the captured output
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
output := buf.String()
|
||||
|
||||
// Verify the output contains the error message
|
||||
assert.Contains(t, output, "[ERROR]")
|
||||
assert.Contains(t, output, "test error message")
|
||||
}
|
||||
|
||||
func TestErrorWithAllLevels(t *testing.T) {
|
||||
// Test that error messages are logged at all levels
|
||||
levels := []string{"debug", "info", "warn", "error"}
|
||||
for _, level := range levels {
|
||||
// Capture stdout
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
// Create logger with current level
|
||||
logger := New(level, "text", "")
|
||||
|
||||
// Log an error message
|
||||
logger.Error("test error message")
|
||||
|
||||
// Restore stdout
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
|
||||
// Read the captured output
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
output := buf.String()
|
||||
|
||||
// Verify the output contains the error message
|
||||
assert.Contains(t, output, "[ERROR]")
|
||||
assert.Contains(t, output, "test error message")
|
||||
}
|
||||
}
|
||||
241
orig/internal/logger/secure_audit.go
Normal file
241
orig/internal/logger/secure_audit.go
Normal file
@@ -0,0 +1,241 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
// FilterMessageEnhanced provides comprehensive filtering with audit logging
|
||||
func (sf *SecureFilter) FilterMessageEnhanced(message string, context map[string]interface{}) string {
|
||||
originalMessage := message
|
||||
filtered := sf.FilterMessage(message)
|
||||
|
||||
// Audit sensitive data detection if enabled
|
||||
if sf.auditEnabled {
|
||||
auditData := sf.detectSensitiveData(originalMessage, context)
|
||||
if len(auditData) > 0 {
|
||||
sf.logAuditEvent(auditData)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
// detectSensitiveData identifies and catalogs sensitive data found in messages
|
||||
func (sf *SecureFilter) detectSensitiveData(message string, context map[string]interface{}) map[string]interface{} {
|
||||
detected := make(map[string]interface{})
|
||||
detected["timestamp"] = time.Now().UTC().Format(time.RFC3339)
|
||||
detected["security_level"] = sf.level
|
||||
|
||||
if context != nil {
|
||||
detected["context"] = context
|
||||
}
|
||||
|
||||
// Check for different types of sensitive data
|
||||
sensitiveTypes := []string{}
|
||||
|
||||
// Check for private keys (CRITICAL)
|
||||
for _, pattern := range sf.privateKeyPatterns {
|
||||
if pattern.MatchString(message) {
|
||||
sensitiveTypes = append(sensitiveTypes, "private_key")
|
||||
detected["severity"] = "CRITICAL"
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Check for transaction hashes BEFORE addresses (64 chars vs 40 chars)
|
||||
for _, pattern := range sf.hashPatterns {
|
||||
if pattern.MatchString(message) {
|
||||
sensitiveTypes = append(sensitiveTypes, "transaction_hash")
|
||||
if detected["severity"] == nil {
|
||||
detected["severity"] = "LOW"
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Check for addresses AFTER hashes
|
||||
for _, pattern := range sf.addressPatterns {
|
||||
if pattern.MatchString(message) {
|
||||
sensitiveTypes = append(sensitiveTypes, "address")
|
||||
if detected["severity"] == nil {
|
||||
detected["severity"] = "MEDIUM"
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Check for amounts/values
|
||||
for _, pattern := range sf.amountPatterns {
|
||||
if pattern.MatchString(message) {
|
||||
sensitiveTypes = append(sensitiveTypes, "amount")
|
||||
if detected["severity"] == nil {
|
||||
detected["severity"] = "LOW"
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(sensitiveTypes) > 0 {
|
||||
detected["types"] = sensitiveTypes
|
||||
detected["message_length"] = len(message)
|
||||
detected["filtered_length"] = len(sf.FilterMessage(message))
|
||||
return detected
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// logAuditEvent logs sensitive data detection events
|
||||
func (sf *SecureFilter) logAuditEvent(auditData map[string]interface{}) {
|
||||
// Create audit log entry
|
||||
auditEntry := map[string]interface{}{
|
||||
"event_type": "sensitive_data_detected",
|
||||
"timestamp": auditData["timestamp"],
|
||||
"severity": auditData["severity"],
|
||||
"types": auditData["types"],
|
||||
"message_stats": map[string]interface{}{
|
||||
"original_length": auditData["message_length"],
|
||||
"filtered_length": auditData["filtered_length"],
|
||||
},
|
||||
}
|
||||
|
||||
if auditData["context"] != nil {
|
||||
auditEntry["context"] = auditData["context"]
|
||||
}
|
||||
|
||||
// Encrypt audit data if encryption is enabled
|
||||
if sf.auditEncryption && len(sf.encryptionKey) > 0 {
|
||||
encrypted, err := sf.encryptAuditData(auditEntry)
|
||||
if err == nil {
|
||||
auditEntry = map[string]interface{}{
|
||||
"encrypted": true,
|
||||
"data": encrypted,
|
||||
"timestamp": auditData["timestamp"],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log to audit trail (this would typically go to a separate audit log file)
|
||||
// For now, we'll add it to a structured format that can be easily extracted
|
||||
auditJSON, _ := json.Marshal(auditEntry)
|
||||
fmt.Printf("AUDIT_LOG: %s\n", string(auditJSON))
|
||||
}
|
||||
|
||||
// encryptAuditData encrypts sensitive audit data
|
||||
func (sf *SecureFilter) encryptAuditData(data map[string]interface{}) (string, error) {
|
||||
if len(sf.encryptionKey) == 0 {
|
||||
return "", fmt.Errorf("no encryption key available")
|
||||
}
|
||||
|
||||
// Serialize data to JSON
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal audit data: %w", err)
|
||||
}
|
||||
|
||||
// Create AES-GCM cipher (AEAD - authenticated encryption)
|
||||
key := sha256.Sum256(sf.encryptionKey)
|
||||
block, err := aes.NewCipher(key[:])
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create cipher: %w", err)
|
||||
}
|
||||
|
||||
// Create GCM instance
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create GCM: %w", err)
|
||||
}
|
||||
|
||||
// Generate random nonce
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return "", fmt.Errorf("failed to generate nonce: %w", err)
|
||||
}
|
||||
|
||||
// Encrypt and authenticate data
|
||||
encrypted := gcm.Seal(nonce, nonce, jsonData, nil)
|
||||
return hex.EncodeToString(encrypted), nil
|
||||
}
|
||||
|
||||
// DecryptAuditData decrypts audit data (for authorized access)
|
||||
func (sf *SecureFilter) DecryptAuditData(encryptedHex string) (map[string]interface{}, error) {
|
||||
if len(sf.encryptionKey) == 0 {
|
||||
return nil, fmt.Errorf("no encryption key available")
|
||||
}
|
||||
|
||||
// Decode hex
|
||||
encryptedData, err := hex.DecodeString(encryptedHex)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode hex: %w", err)
|
||||
}
|
||||
|
||||
// Create AES-GCM cipher (AEAD - authenticated encryption)
|
||||
key := sha256.Sum256(sf.encryptionKey)
|
||||
block, err := aes.NewCipher(key[:])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create cipher: %w", err)
|
||||
}
|
||||
|
||||
// Create GCM instance
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create GCM: %w", err)
|
||||
}
|
||||
|
||||
// Check minimum length (nonce + encrypted data + tag)
|
||||
if len(encryptedData) < gcm.NonceSize() {
|
||||
return nil, fmt.Errorf("encrypted data too short")
|
||||
}
|
||||
|
||||
// Extract nonce and encrypted data
|
||||
nonce := encryptedData[:gcm.NonceSize()]
|
||||
ciphertext := encryptedData[gcm.NonceSize():]
|
||||
|
||||
// Decrypt and authenticate data
|
||||
decrypted, err := gcm.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt data: %w", err)
|
||||
}
|
||||
|
||||
// Unmarshal JSON
|
||||
var result map[string]interface{}
|
||||
err = json.Unmarshal(decrypted, &result)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal decrypted data: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// EnableAuditLogging enables audit logging with optional encryption
|
||||
func (sf *SecureFilter) EnableAuditLogging(encryptionKey []byte) {
|
||||
sf.auditEnabled = true
|
||||
if len(encryptionKey) > 0 {
|
||||
sf.encryptionKey = encryptionKey
|
||||
sf.auditEncryption = true
|
||||
}
|
||||
}
|
||||
|
||||
// DisableAuditLogging disables audit logging
|
||||
func (sf *SecureFilter) DisableAuditLogging() {
|
||||
sf.auditEnabled = false
|
||||
sf.auditEncryption = false
|
||||
}
|
||||
|
||||
// SetSecurityLevel changes the security level dynamically
|
||||
func (sf *SecureFilter) SetSecurityLevel(level SecurityLevel) {
|
||||
sf.level = level
|
||||
}
|
||||
|
||||
// GetSecurityLevel returns the current security level
|
||||
func (sf *SecureFilter) GetSecurityLevel() SecurityLevel {
|
||||
return sf.level
|
||||
}
|
||||
301
orig/internal/logger/secure_filter.go
Normal file
301
orig/internal/logger/secure_filter.go
Normal file
@@ -0,0 +1,301 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
// SecurityLevel defines the logging security level
|
||||
type SecurityLevel int
|
||||
|
||||
const (
|
||||
SecurityLevelDebug SecurityLevel = iota // Log everything (development only)
|
||||
SecurityLevelInfo // Log basic info, filter amounts
|
||||
SecurityLevelProduction // Log minimal info, filter sensitive data
|
||||
)
|
||||
|
||||
// SecureFilter filters sensitive data from log messages
|
||||
type SecureFilter struct {
|
||||
level SecurityLevel
|
||||
|
||||
// Patterns to detect sensitive data
|
||||
amountPatterns []*regexp.Regexp
|
||||
addressPatterns []*regexp.Regexp
|
||||
valuePatterns []*regexp.Regexp
|
||||
hashPatterns []*regexp.Regexp
|
||||
privateKeyPatterns []*regexp.Regexp
|
||||
encryptionKey []byte
|
||||
auditEnabled bool
|
||||
auditEncryption bool
|
||||
}
|
||||
|
||||
// SecureFilterConfig contains configuration for the secure filter
|
||||
type SecureFilterConfig struct {
|
||||
Level SecurityLevel
|
||||
EncryptionKey []byte
|
||||
AuditEnabled bool
|
||||
AuditEncryption bool
|
||||
}
|
||||
|
||||
// NewSecureFilter creates a new secure filter with enhanced configuration
|
||||
func NewSecureFilter(level SecurityLevel) *SecureFilter {
|
||||
return NewSecureFilterWithConfig(&SecureFilterConfig{
|
||||
Level: level,
|
||||
AuditEnabled: false,
|
||||
AuditEncryption: false,
|
||||
})
|
||||
}
|
||||
|
||||
// NewSecureFilterWithConfig creates a new secure filter with full configuration
|
||||
func NewSecureFilterWithConfig(config *SecureFilterConfig) *SecureFilter {
|
||||
return &SecureFilter{
|
||||
level: config.Level,
|
||||
encryptionKey: config.EncryptionKey,
|
||||
auditEnabled: config.AuditEnabled,
|
||||
auditEncryption: config.AuditEncryption,
|
||||
amountPatterns: []*regexp.Regexp{
|
||||
regexp.MustCompile(`(?i)amount[^=]*=\s*[0-9]+`),
|
||||
regexp.MustCompile(`(?i)value[^=]*=\s*[0-9]+`),
|
||||
regexp.MustCompile(`\$[0-9]+\.?[0-9]*`),
|
||||
regexp.MustCompile(`[0-9]+\.[0-9]+ USD`),
|
||||
regexp.MustCompile(`(?i)amountIn=[0-9]+`),
|
||||
regexp.MustCompile(`(?i)amountOut=[0-9]+`),
|
||||
regexp.MustCompile(`(?i)balance[^=]*=\s*[0-9]+`),
|
||||
regexp.MustCompile(`(?i)profit[^=]*=\s*[0-9]+`),
|
||||
regexp.MustCompile(`(?i)gas[Pp]rice[^=]*=\s*[0-9]+`),
|
||||
regexp.MustCompile(`\b[0-9]{15,}\b`), // Very large numbers likely to be wei amounts (but not hex addresses)
|
||||
},
|
||||
addressPatterns: []*regexp.Regexp{
|
||||
regexp.MustCompile(`0x[a-fA-F0-9]{40}`),
|
||||
regexp.MustCompile(`(?i)address[^=]*=\s*0x[a-fA-F0-9]{40}`),
|
||||
regexp.MustCompile(`(?i)contract[^=]*=\s*0x[a-fA-F0-9]{40}`),
|
||||
regexp.MustCompile(`(?i)token[^=]*=\s*0x[a-fA-F0-9]{40}`),
|
||||
},
|
||||
valuePatterns: []*regexp.Regexp{
|
||||
regexp.MustCompile(`(?i)value:\s*\$[0-9]+\.?[0-9]*`),
|
||||
regexp.MustCompile(`(?i)profit[^=]*=\s*\$?[0-9]+\.?[0-9]*`),
|
||||
regexp.MustCompile(`(?i)total:\s*\$[0-9]+\.?[0-9]*`),
|
||||
regexp.MustCompile(`(?i)revenue[^=]*=\s*\$?[0-9]+\.?[0-9]*`),
|
||||
regexp.MustCompile(`(?i)fee[^=]*=\s*\$?[0-9]+\.?[0-9]*`),
|
||||
},
|
||||
hashPatterns: []*regexp.Regexp{
|
||||
regexp.MustCompile(`0x[a-fA-F0-9]{64}`), // Transaction hashes
|
||||
regexp.MustCompile(`(?i)txHash[^=]*=\s*0x[a-fA-F0-9]{64}`),
|
||||
regexp.MustCompile(`(?i)blockHash[^=]*=\s*0x[a-fA-F0-9]{64}`),
|
||||
},
|
||||
privateKeyPatterns: []*regexp.Regexp{
|
||||
regexp.MustCompile(`(?i)private[_\s]*key[^=]*=\s*[a-fA-F0-9]{64}`),
|
||||
regexp.MustCompile(`(?i)secret[^=]*=\s*[a-fA-F0-9]{64}`),
|
||||
regexp.MustCompile(`(?i)mnemonic[^=]*=\s*\"[^\"]*\"`),
|
||||
regexp.MustCompile(`(?i)seed[^=]*=\s*\"[^\"]*\"`),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// FilterMessage filters sensitive data from a log message with enhanced input sanitization
|
||||
func (sf *SecureFilter) FilterMessage(message string) string {
|
||||
if sf.level == SecurityLevelDebug {
|
||||
return sf.sanitizeInput(message) // Still sanitize for security even in debug mode
|
||||
}
|
||||
|
||||
filtered := sf.sanitizeInput(message)
|
||||
|
||||
// Filter private keys FIRST (highest security priority)
|
||||
for _, pattern := range sf.privateKeyPatterns {
|
||||
filtered = pattern.ReplaceAllString(filtered, "[PRIVATE_KEY_FILTERED]")
|
||||
}
|
||||
|
||||
// Filter transaction hashes
|
||||
if sf.level >= SecurityLevelInfo {
|
||||
for _, pattern := range sf.hashPatterns {
|
||||
filtered = pattern.ReplaceAllStringFunc(filtered, func(hash string) string {
|
||||
if len(hash) == 66 { // Full transaction hash
|
||||
return hash[:10] + "..." + hash[62:] // Show first 8 and last 4 chars
|
||||
}
|
||||
return "[HASH_FILTERED]"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Filter addresses NEXT (before amounts) to prevent addresses from being treated as numbers
|
||||
if sf.level >= SecurityLevelProduction {
|
||||
for _, pattern := range sf.addressPatterns {
|
||||
filtered = pattern.ReplaceAllStringFunc(filtered, func(addr string) string {
|
||||
if len(addr) == 42 { // Full Ethereum address
|
||||
return addr[:6] + "..." + addr[38:] // Show first 4 and last 4 chars
|
||||
}
|
||||
return "[ADDR_FILTERED]"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Filter amounts LAST
|
||||
if sf.level >= SecurityLevelInfo {
|
||||
for _, pattern := range sf.amountPatterns {
|
||||
filtered = pattern.ReplaceAllString(filtered, "[AMOUNT_FILTERED]")
|
||||
}
|
||||
|
||||
for _, pattern := range sf.valuePatterns {
|
||||
filtered = pattern.ReplaceAllString(filtered, "[VALUE_FILTERED]")
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
// sanitizeInput performs comprehensive input sanitization for log messages
|
||||
func (sf *SecureFilter) sanitizeInput(input string) string {
|
||||
// Remove null bytes and other control characters that could cause issues
|
||||
sanitized := strings.ReplaceAll(input, "\x00", "")
|
||||
sanitized = strings.ReplaceAll(sanitized, "\r", "")
|
||||
|
||||
// Remove potential log injection patterns
|
||||
sanitized = strings.ReplaceAll(sanitized, "\n", " ") // Replace newlines with spaces
|
||||
sanitized = strings.ReplaceAll(sanitized, "\t", " ") // Replace tabs with spaces
|
||||
|
||||
// Remove ANSI escape codes that could interfere with log parsing
|
||||
ansiPattern := regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`)
|
||||
sanitized = ansiPattern.ReplaceAllString(sanitized, "")
|
||||
|
||||
// Remove other control characters (keep only printable ASCII and common Unicode)
|
||||
controlPattern := regexp.MustCompile(`[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]`)
|
||||
sanitized = controlPattern.ReplaceAllString(sanitized, "")
|
||||
|
||||
// Prevent log injection by escaping special characters
|
||||
sanitized = strings.ReplaceAll(sanitized, "%", "%%") // Escape printf format specifiers
|
||||
|
||||
// Limit message length to prevent DoS via large log messages
|
||||
const maxLogMessageLength = 4096
|
||||
if len(sanitized) > maxLogMessageLength {
|
||||
sanitized = sanitized[:maxLogMessageLength-3] + "..."
|
||||
}
|
||||
|
||||
return sanitized
|
||||
}
|
||||
|
||||
// FilterTransactionData provides enhanced filtering for transaction data logging
|
||||
func (sf *SecureFilter) FilterTransactionData(txHash, from, to string, value, gasPrice *big.Int, data []byte) map[string]interface{} {
|
||||
result := map[string]interface{}{}
|
||||
|
||||
// Always include sanitized transaction hash
|
||||
result["tx_hash"] = sf.sanitizeInput(txHash)
|
||||
|
||||
switch sf.level {
|
||||
case SecurityLevelDebug:
|
||||
result["from"] = sf.sanitizeInput(from)
|
||||
result["to"] = sf.sanitizeInput(to)
|
||||
if value != nil {
|
||||
result["value"] = value.String()
|
||||
}
|
||||
if gasPrice != nil {
|
||||
result["gas_price"] = gasPrice.String()
|
||||
}
|
||||
result["data_size"] = len(data)
|
||||
|
||||
case SecurityLevelInfo:
|
||||
result["from"] = sf.shortenAddress(common.HexToAddress(from))
|
||||
result["to"] = sf.shortenAddress(common.HexToAddress(to))
|
||||
if value != nil {
|
||||
result["value_range"] = sf.getAmountRange(value)
|
||||
}
|
||||
if gasPrice != nil {
|
||||
result["gas_price_range"] = sf.getAmountRange(gasPrice)
|
||||
}
|
||||
result["data_size"] = len(data)
|
||||
|
||||
case SecurityLevelProduction:
|
||||
result["has_from"] = from != ""
|
||||
result["has_to"] = to != ""
|
||||
result["has_value"] = value != nil && value.Sign() > 0
|
||||
result["data_size"] = len(data)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// FilterSwapData creates a safe representation of swap data for logging
|
||||
func (sf *SecureFilter) FilterSwapData(tokenIn, tokenOut common.Address, amountIn, amountOut *big.Int, protocol string) map[string]interface{} {
|
||||
data := map[string]interface{}{
|
||||
"protocol": protocol,
|
||||
}
|
||||
|
||||
switch sf.level {
|
||||
case SecurityLevelDebug:
|
||||
data["tokenIn"] = tokenIn.Hex()
|
||||
data["tokenOut"] = tokenOut.Hex()
|
||||
data["amountIn"] = amountIn.String()
|
||||
data["amountOut"] = amountOut.String()
|
||||
|
||||
case SecurityLevelInfo:
|
||||
data["tokenInShort"] = sf.shortenAddress(tokenIn)
|
||||
data["tokenOutShort"] = sf.shortenAddress(tokenOut)
|
||||
data["amountRange"] = sf.getAmountRange(amountIn)
|
||||
data["amountOutRange"] = sf.getAmountRange(amountOut)
|
||||
|
||||
case SecurityLevelProduction:
|
||||
data["tokenCount"] = 2
|
||||
data["hasAmounts"] = amountIn != nil && amountOut != nil
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// shortenAddress returns a shortened version of an address
|
||||
func (sf *SecureFilter) shortenAddress(addr common.Address) string {
|
||||
hex := addr.Hex()
|
||||
if len(hex) >= 10 {
|
||||
return hex[:6] + "..." + hex[len(hex)-4:]
|
||||
}
|
||||
return "[ADDR]"
|
||||
}
|
||||
|
||||
// getAmountRange returns a range category for an amount
|
||||
func (sf *SecureFilter) getAmountRange(amount *big.Int) string {
|
||||
if amount == nil {
|
||||
return "nil"
|
||||
}
|
||||
|
||||
// Convert to rough USD equivalent (assuming 18 decimals)
|
||||
usdValue := new(big.Float).Quo(new(big.Float).SetInt(amount), big.NewFloat(1e18))
|
||||
usdFloat, _ := usdValue.Float64()
|
||||
|
||||
switch {
|
||||
case usdFloat < 1:
|
||||
return "micro"
|
||||
case usdFloat < 100:
|
||||
return "small"
|
||||
case usdFloat < 10000:
|
||||
return "medium"
|
||||
case usdFloat < 1000000:
|
||||
return "large"
|
||||
default:
|
||||
return "whale"
|
||||
}
|
||||
}
|
||||
|
||||
// SanitizeForProduction removes all sensitive data for production logging
|
||||
func (sf *SecureFilter) SanitizeForProduction(data map[string]interface{}) map[string]interface{} {
|
||||
sanitized := make(map[string]interface{})
|
||||
|
||||
for key, value := range data {
|
||||
switch strings.ToLower(key) {
|
||||
case "amount", "amountin", "amountout", "value", "profit", "usd", "price":
|
||||
sanitized[key] = "[FILTERED]"
|
||||
case "address", "token", "tokenin", "tokenout", "pool", "contract":
|
||||
if addr, ok := value.(common.Address); ok {
|
||||
sanitized[key] = sf.shortenAddress(addr)
|
||||
} else if str, ok := value.(string); ok && strings.HasPrefix(str, "0x") && len(str) == 42 {
|
||||
sanitized[key] = str[:6] + "..." + str[38:]
|
||||
} else {
|
||||
sanitized[key] = value
|
||||
}
|
||||
default:
|
||||
sanitized[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized
|
||||
}
|
||||
226
orig/internal/logger/secure_filter_enhanced_test.go
Normal file
226
orig/internal/logger/secure_filter_enhanced_test.go
Normal file
@@ -0,0 +1,226 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
func TestSecureFilterEnhanced(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
level SecurityLevel
|
||||
message string
|
||||
expectFiltered bool
|
||||
expectedLevel string
|
||||
}{
|
||||
{
|
||||
name: "Private key detection",
|
||||
level: SecurityLevelProduction,
|
||||
message: "private_key=1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
||||
expectFiltered: true,
|
||||
expectedLevel: "CRITICAL",
|
||||
},
|
||||
{
|
||||
name: "Address detection",
|
||||
level: SecurityLevelProduction,
|
||||
message: "Swapping on address 0x1234567890123456789012345678901234567890",
|
||||
expectFiltered: true,
|
||||
expectedLevel: "MEDIUM",
|
||||
},
|
||||
{
|
||||
name: "Amount detection",
|
||||
level: SecurityLevelInfo,
|
||||
message: "Profit amount=1000000 wei detected",
|
||||
expectFiltered: true,
|
||||
expectedLevel: "LOW",
|
||||
},
|
||||
{
|
||||
name: "Transaction hash detection",
|
||||
level: SecurityLevelInfo,
|
||||
message: "txHash=0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
||||
expectFiltered: true,
|
||||
expectedLevel: "LOW",
|
||||
},
|
||||
{
|
||||
name: "No sensitive data",
|
||||
level: SecurityLevelProduction,
|
||||
message: "Normal log message with no sensitive data",
|
||||
expectFiltered: false,
|
||||
expectedLevel: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
config := &SecureFilterConfig{
|
||||
Level: tt.level,
|
||||
AuditEnabled: true,
|
||||
AuditEncryption: false,
|
||||
}
|
||||
filter := NewSecureFilterWithConfig(config)
|
||||
|
||||
// Test detection without filtering yet
|
||||
auditData := filter.detectSensitiveData(tt.message, nil)
|
||||
|
||||
if tt.expectFiltered {
|
||||
if auditData == nil {
|
||||
t.Errorf("Expected sensitive data detection for: %s", tt.message)
|
||||
return
|
||||
}
|
||||
if auditData["severity"] != tt.expectedLevel {
|
||||
t.Errorf("Expected severity %s, got %v", tt.expectedLevel, auditData["severity"])
|
||||
}
|
||||
} else {
|
||||
if auditData != nil {
|
||||
t.Errorf("Unexpected sensitive data detection for: %s", tt.message)
|
||||
}
|
||||
}
|
||||
|
||||
// Test the actual filtering
|
||||
filtered := filter.FilterMessage(tt.message)
|
||||
if tt.expectFiltered && filtered == tt.message {
|
||||
t.Errorf("Expected message to be filtered, but it wasn't: %s", tt.message)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecureFilterWithEncryption(t *testing.T) {
|
||||
// Generate a random encryption key
|
||||
key := make([]byte, 32)
|
||||
_, err := rand.Read(key)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate encryption key: %v", err)
|
||||
}
|
||||
|
||||
config := &SecureFilterConfig{
|
||||
Level: SecurityLevelProduction,
|
||||
EncryptionKey: key,
|
||||
AuditEnabled: true,
|
||||
AuditEncryption: true,
|
||||
}
|
||||
filter := NewSecureFilterWithConfig(config)
|
||||
|
||||
testData := map[string]interface{}{
|
||||
"test_field": "test_value",
|
||||
"number": 123,
|
||||
"nested": map[string]interface{}{
|
||||
"inner": "value",
|
||||
},
|
||||
}
|
||||
|
||||
// Test encryption and decryption
|
||||
encrypted, err := filter.encryptAuditData(testData)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to encrypt audit data: %v", err)
|
||||
}
|
||||
|
||||
if encrypted == "" {
|
||||
t.Fatal("Encrypted data should not be empty")
|
||||
}
|
||||
|
||||
// Test decryption
|
||||
decrypted, err := filter.DecryptAuditData(encrypted)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to decrypt audit data: %v", err)
|
||||
}
|
||||
|
||||
// Verify data integrity
|
||||
if decrypted["test_field"] != testData["test_field"] {
|
||||
t.Errorf("Decrypted data doesn't match original")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecureFilterAddressFiltering(t *testing.T) {
|
||||
filter := NewSecureFilter(SecurityLevelProduction)
|
||||
|
||||
address := common.HexToAddress("0x1234567890123456789012345678901234567890")
|
||||
testMessage := "Processing transaction for address " + address.Hex()
|
||||
|
||||
filtered := filter.FilterMessage(testMessage)
|
||||
|
||||
// Should contain shortened address
|
||||
if !strings.Contains(filtered, "0x1234...7890") {
|
||||
t.Errorf("Expected shortened address in filtered message, got: %s", filtered)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecureFilterAmountFiltering(t *testing.T) {
|
||||
filter := NewSecureFilter(SecurityLevelInfo)
|
||||
|
||||
testCases := []struct {
|
||||
message string
|
||||
contains string
|
||||
}{
|
||||
{"amount=1000000", "[AMOUNT_FILTERED]"},
|
||||
{"Profit $123.45 detected", "[AMOUNT_FILTERED]"},
|
||||
{"balance=999999999999999999", "[AMOUNT_FILTERED]"},
|
||||
{"gasPrice=20000000000", "[AMOUNT_FILTERED]"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
filtered := filter.FilterMessage(tc.message)
|
||||
if !strings.Contains(filtered, tc.contains) {
|
||||
t.Errorf("Expected %s in filtered message for input '%s', got: %s", tc.contains, tc.message, filtered)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecureFilterConfiguration(t *testing.T) {
|
||||
filter := NewSecureFilter(SecurityLevelDebug)
|
||||
|
||||
// Test initial level
|
||||
if filter.GetSecurityLevel() != SecurityLevelDebug {
|
||||
t.Errorf("Expected initial level to be Debug, got: %v", filter.GetSecurityLevel())
|
||||
}
|
||||
|
||||
// Test level change
|
||||
filter.SetSecurityLevel(SecurityLevelProduction)
|
||||
if filter.GetSecurityLevel() != SecurityLevelProduction {
|
||||
t.Errorf("Expected level to be Production, got: %v", filter.GetSecurityLevel())
|
||||
}
|
||||
|
||||
// Test audit enabling
|
||||
filter.EnableAuditLogging([]byte("test-key"))
|
||||
if !filter.auditEnabled {
|
||||
t.Error("Expected audit to be enabled")
|
||||
}
|
||||
|
||||
if !filter.auditEncryption {
|
||||
t.Error("Expected audit encryption to be enabled")
|
||||
}
|
||||
|
||||
// Test audit disabling
|
||||
filter.DisableAuditLogging()
|
||||
if filter.auditEnabled {
|
||||
t.Error("Expected audit to be disabled")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSecureFilter(b *testing.B) {
|
||||
filter := NewSecureFilter(SecurityLevelProduction)
|
||||
testMessage := "Processing transaction 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef for address 0x1111111111111111111111111111111111111111 with amount=1000000"
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
filter.FilterMessage(testMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSecureFilterEnhanced(b *testing.B) {
|
||||
config := &SecureFilterConfig{
|
||||
Level: SecurityLevelProduction,
|
||||
AuditEnabled: true,
|
||||
AuditEncryption: false,
|
||||
}
|
||||
filter := NewSecureFilterWithConfig(config)
|
||||
testMessage := "Processing transaction 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef for address 0x1111111111111111111111111111111111111111 with amount=1000000"
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
filter.FilterMessageEnhanced(testMessage, nil)
|
||||
}
|
||||
}
|
||||
439
orig/internal/logger/secure_filter_test.go
Normal file
439
orig/internal/logger/secure_filter_test.go
Normal file
@@ -0,0 +1,439 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewSecureFilter(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
level SecurityLevel
|
||||
}{
|
||||
{"Debug level", SecurityLevelDebug},
|
||||
{"Info level", SecurityLevelInfo},
|
||||
{"Production level", SecurityLevelProduction},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
filter := NewSecureFilter(tt.level)
|
||||
assert.NotNil(t, filter)
|
||||
assert.Equal(t, tt.level, filter.level)
|
||||
assert.NotNil(t, filter.amountPatterns)
|
||||
assert.NotNil(t, filter.addressPatterns)
|
||||
assert.NotNil(t, filter.valuePatterns)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterMessage_DebugLevel(t *testing.T) {
|
||||
filter := NewSecureFilter(SecurityLevelDebug)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Debug shows everything",
|
||||
input: "Amount: 1000.5 ETH, Address: 0x742d35Cc6AaB8f5d6649c8C4F7C6b2d123456789, Value: $5000.00",
|
||||
expected: "Amount: 1000.5 ETH, Address: 0x742d35Cc6AaB8f5d6649c8C4F7C6b2d123456789, Value: $5000.00",
|
||||
},
|
||||
{
|
||||
name: "Large amounts shown in debug",
|
||||
input: "Profit: 999999.123456789 USDC",
|
||||
expected: "Profit: 999999.123456789 USDC",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := filter.FilterMessage(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterMessage_InfoLevel(t *testing.T) {
|
||||
filter := NewSecureFilter(SecurityLevelInfo)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Info filters amounts but shows full addresses",
|
||||
input: "Amount: 1000.5 ETH, Address: 0x742d35Cc6AaB8f5d6649c8C4F7C6b2d123456789",
|
||||
expected: "Amount: 1000.5 ETH, Address: 0x742d35Cc6AaB8f5d6649c8C4F7C6b2d123456789",
|
||||
},
|
||||
{
|
||||
name: "USD values filtered",
|
||||
input: "Profit: $5000.00 USD",
|
||||
expected: "Profit: [AMOUNT_FILTERED] USD",
|
||||
},
|
||||
{
|
||||
name: "Multiple amounts filtered",
|
||||
input: "Swap 100.0 USDC for 0.05 ETH",
|
||||
expected: "Swap [AMOUNT_FILTERED]C for 0.05 ETH",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := filter.FilterMessage(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterMessage_ProductionLevel(t *testing.T) {
|
||||
filter := NewSecureFilter(SecurityLevelProduction)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Production filters everything sensitive",
|
||||
input: "Amount: 1000.5 ETH, Address: 0x742d35Cc6AaB8f5d6649c8C4F7C6b2d123456789, Value: $5000.00",
|
||||
expected: "Amount: 1000.5 ETH, Address: 0x742d...6789, Value: [AMOUNT_FILTERED]",
|
||||
},
|
||||
{
|
||||
name: "Complex transaction filtered",
|
||||
input: "Swap 1500.789 USDC from 0xA0b86a33E6441f43E2e4A96439abFA2A69067ACD to 0xaf88d065e77c8cC2239327C5EDb3A432268e5831 for $1500.00 profit",
|
||||
expected: "Swap [AMOUNT_FILTERED]C from 0xA0b8...7ACD to 0xaf88...5831 for [AMOUNT_FILTERED] profit",
|
||||
},
|
||||
{
|
||||
name: "Multiple addresses and amounts",
|
||||
input: "Transfer 500 tokens from 0x1234567890123456789012345678901234567890 to 0x0987654321098765432109876543210987654321 worth $1000",
|
||||
expected: "Transfer 500 tokens from 0x1234...7890 to 0x0987...4321 worth [AMOUNT_FILTERED]",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := filter.FilterMessage(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShortenAddress(t *testing.T) {
|
||||
filter := NewSecureFilter(SecurityLevelInfo)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input common.Address
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Standard address",
|
||||
input: common.HexToAddress("0x742d35Cc6AaB8f5d6649c8C4F7C6b2d123456789"),
|
||||
expected: "0x742D...6789",
|
||||
},
|
||||
{
|
||||
name: "Zero address",
|
||||
input: common.HexToAddress("0x0000000000000000000000000000000000000000"),
|
||||
expected: "0x0000...0000",
|
||||
},
|
||||
{
|
||||
name: "Max address",
|
||||
input: common.HexToAddress("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"),
|
||||
expected: "0xFFfF...FFfF",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := filter.shortenAddress(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCategorizeAmount(t *testing.T) {
|
||||
_ = NewSecureFilter(SecurityLevelInfo) // Reference to avoid unused variable warning
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input *big.Int
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Nil amount",
|
||||
input: nil,
|
||||
expected: "nil",
|
||||
},
|
||||
{
|
||||
name: "Micro amount (< $1)",
|
||||
input: big.NewInt(500000000000000000), // 0.5 ETH assuming 18 decimals
|
||||
expected: "micro",
|
||||
},
|
||||
{
|
||||
name: "Small amount ($1-$100)",
|
||||
input: func() *big.Int { val, _ := new(big.Int).SetString("50000000000000000000", 10); return val }(), // 50 ETH
|
||||
expected: "small",
|
||||
},
|
||||
{
|
||||
name: "Medium amount ($100-$10k)",
|
||||
input: func() *big.Int { val, _ := new(big.Int).SetString("5000000000000000000000", 10); return val }(), // 5000 ETH
|
||||
expected: "medium",
|
||||
},
|
||||
{
|
||||
name: "Large amount ($10k-$1M)",
|
||||
input: func() *big.Int { val, _ := new(big.Int).SetString("500000000000000000000000", 10); return val }(), // 500k ETH
|
||||
expected: "large",
|
||||
},
|
||||
{
|
||||
name: "Whale amount (>$1M)",
|
||||
input: func() *big.Int { val, _ := new(big.Int).SetString("2000000000000000000000000", 10); return val }(), // 2M ETH
|
||||
expected: "whale",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Note: categorizeAmount is private, so we can't test it directly
|
||||
// This test would need to be adapted to test the public API that uses it
|
||||
_ = tt.input // Reference to avoid unused variable warning
|
||||
_ = tt.expected // Reference to avoid unused variable warning
|
||||
// Just pass the test since we can't test private methods directly
|
||||
assert.True(t, true, "categorizeAmount is private - testing would need public wrapper")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeForProduction(t *testing.T) {
|
||||
filter := NewSecureFilter(SecurityLevelProduction)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input map[string]interface{}
|
||||
expected map[string]interface{}
|
||||
}{
|
||||
{
|
||||
name: "Amounts filtered",
|
||||
input: map[string]interface{}{
|
||||
"amount": 1000.5,
|
||||
"amountIn": 500,
|
||||
"amountOut": 750,
|
||||
"value": 999.99,
|
||||
"other": "should remain",
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"amount": "[FILTERED]",
|
||||
"amountIn": "[FILTERED]",
|
||||
"amountOut": "[FILTERED]",
|
||||
"value": "[FILTERED]",
|
||||
"other": "should remain",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Addresses filtered and shortened",
|
||||
input: map[string]interface{}{
|
||||
"address": common.HexToAddress("0x742d35Cc6AaB8f5d6649c8C4F7C6b2d123456789"),
|
||||
"token": "0xA0b86a33E6441f43E2e4A96439abFA2A69067ACD",
|
||||
"pool": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
|
||||
"other": "should remain",
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"address": "0x742D...6789",
|
||||
"token": "0xA0b8...7ACD",
|
||||
"pool": "0xaf88...5831",
|
||||
"other": "should remain",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Mixed data types",
|
||||
input: map[string]interface{}{
|
||||
"profit": 1000.0,
|
||||
"tokenOut": common.HexToAddress("0x742d35Cc6AaB8f5d6649c8C4F7C6b2d123456789"),
|
||||
"fee": 30,
|
||||
"protocol": "UniswapV3",
|
||||
"timestamp": 1640995200,
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"profit": "[FILTERED]",
|
||||
"tokenOut": "0x742D...6789",
|
||||
"fee": 30,
|
||||
"protocol": "UniswapV3",
|
||||
"timestamp": 1640995200,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := filter.SanitizeForProduction(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterMessage_ComplexScenarios(t *testing.T) {
|
||||
productionFilter := NewSecureFilter(SecurityLevelProduction)
|
||||
infoFilter := NewSecureFilter(SecurityLevelInfo)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
production string
|
||||
info string
|
||||
}{
|
||||
{
|
||||
name: "MEV opportunity log",
|
||||
input: "🎯 ARBITRAGE OPPORTUNITY: Swap 1500.789 USDC via 0xA0b86a33E6441f43E2e4A96439abFA2A69067ACD for profit $250.50",
|
||||
production: "🎯 ARBITRAGE OPPORTUNITY: Swap [AMOUNT_FILTERED]C via 0xA0b8...7ACD for profit [AMOUNT_FILTERED]",
|
||||
info: "🎯 ARBITRAGE OPPORTUNITY: Swap [AMOUNT_FILTERED]C via 0xA0b86a33E6441f43E2e4A96439abFA2A69067ACD for profit [AMOUNT_FILTERED]",
|
||||
},
|
||||
{
|
||||
name: "Transaction log with multiple sensitive data",
|
||||
input: "TX: 0x123...abc Amount: 999.123456 ETH → 1500000.5 USDC, Gas: 150000, Value: $2500000.75",
|
||||
production: "TX: 0x123...abc Amount: 999.123456 ETH → [AMOUNT_FILTERED]C, Gas: 150000, Value: [AMOUNT_FILTERED]",
|
||||
info: "TX: 0x123...abc Amount: 999.123456 ETH → [AMOUNT_FILTERED]C, Gas: 150000, Value: [AMOUNT_FILTERED]",
|
||||
},
|
||||
{
|
||||
name: "Pool creation event",
|
||||
input: "Pool created: 0x742d35Cc6AaB8f5d6649c8C4F7C6b2d123456789 with 1000000.0 liquidity worth $5000000",
|
||||
production: "Pool created: 0x742d...6789 with 1000000.0 liquidity worth [AMOUNT_FILTERED]",
|
||||
info: "Pool created: 0x742d35Cc6AaB8f5d6649c8C4F7C6b2d123456789 with 1000000.0 liquidity worth [AMOUNT_FILTERED]",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name+" - Production", func(t *testing.T) {
|
||||
result := productionFilter.FilterMessage(tt.input)
|
||||
assert.Equal(t, tt.production, result)
|
||||
})
|
||||
|
||||
t.Run(tt.name+" - Info", func(t *testing.T) {
|
||||
result := infoFilter.FilterMessage(tt.input)
|
||||
assert.Equal(t, tt.info, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterMessage_EdgeCases(t *testing.T) {
|
||||
filter := NewSecureFilter(SecurityLevelProduction)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Empty string",
|
||||
input: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "No sensitive data",
|
||||
input: "Simple log message with no sensitive information",
|
||||
expected: "Simple log message with no sensitive information",
|
||||
},
|
||||
{
|
||||
name: "Only numbers (not amounts)",
|
||||
input: "Block number: 12345, Gas limit: 8000000",
|
||||
expected: "Block number: 12345, Gas limit: 8000000",
|
||||
},
|
||||
{
|
||||
name: "Scientific notation",
|
||||
input: "Amount: 1.5e18 wei",
|
||||
expected: "Amount: 1.5e18 wei",
|
||||
},
|
||||
{
|
||||
name: "Multiple decimal places",
|
||||
input: "Price: 1234.567890123456 tokens",
|
||||
expected: "Price: 1234.567890123456 tokens",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := filter.FilterMessage(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkFilterMessage_Production(b *testing.B) {
|
||||
filter := NewSecureFilter(SecurityLevelProduction)
|
||||
input := "🎯 ARBITRAGE OPPORTUNITY: Swap 1500.789 USDC via 0xA0b86a33E6441f43E2e4A96439abFA2A69067ACD for profit $250.50"
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
filter.FilterMessage(input)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFilterMessage_Info(b *testing.B) {
|
||||
filter := NewSecureFilter(SecurityLevelInfo)
|
||||
input := "Transaction: 1000.5 ETH from 0x742d35Cc6AaB8f5d6649c8C4F7C6b2d123456789 to 0xA0b86a33E6441f43E2e4A96439abFA2A69067ACD"
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
filter.FilterMessage(input)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSanitizeForProduction(b *testing.B) {
|
||||
filter := NewSecureFilter(SecurityLevelProduction)
|
||||
data := map[string]interface{}{
|
||||
"amount": 1000.5,
|
||||
"address": common.HexToAddress("0x742d35Cc6AaB8f5d6649c8C4F7C6b2d123456789"),
|
||||
"profit": 250.75,
|
||||
"protocol": "UniswapV3",
|
||||
"token": "0xA0b86a33E6441f43E2e4A96439abFA2A69067ACD",
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
filter.SanitizeForProduction(data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecurityLevelConstants(t *testing.T) {
|
||||
// Verify security level constants are defined correctly
|
||||
assert.Equal(t, SecurityLevel(0), SecurityLevelDebug)
|
||||
assert.Equal(t, SecurityLevel(1), SecurityLevelInfo)
|
||||
assert.Equal(t, SecurityLevel(2), SecurityLevelProduction)
|
||||
}
|
||||
|
||||
func TestRegexPatterns(t *testing.T) {
|
||||
filter := NewSecureFilter(SecurityLevelProduction)
|
||||
|
||||
// Test that patterns are properly compiled
|
||||
assert.True(t, len(filter.amountPatterns) > 0, "Should have amount patterns")
|
||||
assert.True(t, len(filter.addressPatterns) > 0, "Should have address patterns")
|
||||
assert.True(t, len(filter.valuePatterns) > 0, "Should have value patterns")
|
||||
|
||||
// Test pattern matching
|
||||
testCases := []struct {
|
||||
patterns []*regexp.Regexp
|
||||
input string
|
||||
should string
|
||||
}{
|
||||
{filter.amountPatterns, "amount=123", "match amount patterns"},
|
||||
{filter.addressPatterns, "Address: 0x742d35Cc6AaB8f5d6649c8C4F7C6b2d123456789", "match address patterns"},
|
||||
{filter.valuePatterns, "profit=$1234.56", "match value patterns"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
found := false
|
||||
for _, pattern := range tc.patterns {
|
||||
if pattern.MatchString(tc.input) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, found, tc.should)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user