Files
mev-beta/pkg/security/error_handler.go
2025-10-04 09:31:02 -05:00

349 lines
10 KiB
Go

package security
import (
"context"
"fmt"
"regexp"
"runtime"
"strings"
"time"
"github.com/fraktal/mev-beta/internal/logger"
)
// SecureError represents a security-aware error with context
type SecureError struct {
Code string `json:"code"`
Message string `json:"message"`
Timestamp time.Time `json:"timestamp"`
Context map[string]interface{} `json:"context,omitempty"`
Stack []StackFrame `json:"stack,omitempty"`
Wrapped error `json:"-"`
Sensitive bool `json:"sensitive"`
Category ErrorCategory `json:"category"`
Severity ErrorSeverity `json:"severity"`
}
// StackFrame represents a single frame in the call stack
type StackFrame struct {
Function string `json:"function"`
File string `json:"file"`
Line int `json:"line"`
}
// ErrorCategory defines categories of errors
type ErrorCategory string
const (
ErrorCategoryAuthentication ErrorCategory = "authentication"
ErrorCategoryAuthorization ErrorCategory = "authorization"
ErrorCategoryValidation ErrorCategory = "validation"
ErrorCategoryRateLimit ErrorCategory = "rate_limit"
ErrorCategoryCircuitBreaker ErrorCategory = "circuit_breaker"
ErrorCategoryEncryption ErrorCategory = "encryption"
ErrorCategoryNetwork ErrorCategory = "network"
ErrorCategoryTransaction ErrorCategory = "transaction"
ErrorCategoryInternal ErrorCategory = "internal"
)
// ErrorSeverity defines error severity levels
type ErrorSeverity string
const (
ErrorSeverityLow ErrorSeverity = "low"
ErrorSeverityMedium ErrorSeverity = "medium"
ErrorSeverityHigh ErrorSeverity = "high"
ErrorSeverityCritical ErrorSeverity = "critical"
)
// ErrorHandler provides secure error handling with context preservation
type ErrorHandler struct {
enableStackTrace bool
sensitiveFields map[string]bool
errorMetrics *ErrorMetrics
logger *logger.Logger
}
// ErrorMetrics tracks error statistics
type ErrorMetrics struct {
TotalErrors int64 `json:"total_errors"`
ErrorsByCategory map[ErrorCategory]int64 `json:"errors_by_category"`
ErrorsBySeverity map[ErrorSeverity]int64 `json:"errors_by_severity"`
SensitiveDataLeaks int64 `json:"sensitive_data_leaks"`
}
// NewErrorHandler creates a new secure error handler
func NewErrorHandler(enableStackTrace bool) *ErrorHandler {
return &ErrorHandler{
enableStackTrace: enableStackTrace,
sensitiveFields: map[string]bool{
"password": true,
"private_key": true,
"secret": true,
"token": true,
"seed": true,
"mnemonic": true,
"api_key": true,
"private": true,
},
errorMetrics: &ErrorMetrics{
ErrorsByCategory: make(map[ErrorCategory]int64),
ErrorsBySeverity: make(map[ErrorSeverity]int64),
},
logger: logger.New("info", "json", "logs/errors.log"),
}
}
// WrapError wraps an error with security context
func (eh *ErrorHandler) WrapError(err error, code string, message string, category ErrorCategory, severity ErrorSeverity) *SecureError {
if err == nil {
return nil
}
secureErr := &SecureError{
Code: code,
Message: message,
Timestamp: time.Now(),
Wrapped: err,
Category: category,
Severity: severity,
Context: make(map[string]interface{}),
}
// Capture stack trace if enabled
if eh.enableStackTrace {
secureErr.Stack = eh.captureStackTrace()
}
// Check for sensitive data
secureErr.Sensitive = eh.containsSensitiveData(err.Error()) || eh.containsSensitiveData(message)
// Update metrics
eh.updateMetrics(secureErr)
// Log error appropriately
eh.logError(secureErr)
return secureErr
}
// WrapErrorWithContext wraps an error with additional context
func (eh *ErrorHandler) WrapErrorWithContext(ctx context.Context, err error, code string, message string, category ErrorCategory, severity ErrorSeverity, context map[string]interface{}) *SecureError {
secureErr := eh.WrapError(err, code, message, category, severity)
if secureErr == nil {
return nil
}
// Add context while sanitizing sensitive data
for key, value := range context {
if !eh.isSensitiveField(key) {
secureErr.Context[key] = value
} else {
secureErr.Context[key] = "[REDACTED]"
secureErr.Sensitive = true
}
}
// Add request context if available
if ctx != nil {
if requestID := ctx.Value("request_id"); requestID != nil {
secureErr.Context["request_id"] = requestID
}
if userID := ctx.Value("user_id"); userID != nil {
secureErr.Context["user_id"] = userID
}
if sessionID := ctx.Value("session_id"); sessionID != nil {
secureErr.Context["session_id"] = sessionID
}
}
return secureErr
}
// Error implements the error interface
func (se *SecureError) Error() string {
if se.Sensitive {
return fmt.Sprintf("[%s] %s (sensitive data redacted)", se.Code, se.Message)
}
return fmt.Sprintf("[%s] %s", se.Code, se.Message)
}
// Unwrap returns the wrapped error
func (se *SecureError) Unwrap() error {
return se.Wrapped
}
// SafeString returns a safe string representation without sensitive data
func (se *SecureError) SafeString() string {
if se.Sensitive {
return fmt.Sprintf("Error: %s (details redacted for security)", se.Message)
}
return se.Error()
}
// DetailedString returns detailed error information for internal logging
func (se *SecureError) DetailedString() string {
var parts []string
parts = append(parts, fmt.Sprintf("Code: %s", se.Code))
parts = append(parts, fmt.Sprintf("Message: %s", se.Message))
parts = append(parts, fmt.Sprintf("Category: %s", se.Category))
parts = append(parts, fmt.Sprintf("Severity: %s", se.Severity))
parts = append(parts, fmt.Sprintf("Timestamp: %s", se.Timestamp.Format(time.RFC3339)))
if len(se.Context) > 0 {
parts = append(parts, fmt.Sprintf("Context: %+v", se.Context))
}
if se.Wrapped != nil {
parts = append(parts, fmt.Sprintf("Wrapped: %s", se.Wrapped.Error()))
}
return strings.Join(parts, ", ")
}
// captureStackTrace captures the current call stack
func (eh *ErrorHandler) captureStackTrace() []StackFrame {
var frames []StackFrame
// Skip the first few frames (this function and WrapError)
for i := 3; i < 10; i++ {
pc, file, line, ok := runtime.Caller(i)
if !ok {
break
}
fn := runtime.FuncForPC(pc)
if fn == nil {
continue
}
frames = append(frames, StackFrame{
Function: fn.Name(),
File: file,
Line: line,
})
}
return frames
}
// containsSensitiveData checks if the text contains sensitive information
func (eh *ErrorHandler) containsSensitiveData(text string) bool {
lowercaseText := strings.ToLower(text)
for field := range eh.sensitiveFields {
if strings.Contains(lowercaseText, field) {
return true
}
}
// Check for common patterns that might contain sensitive data
sensitivePatterns := []string{
"0x[a-fA-F0-9]{40}", // Ethereum addresses
"0x[a-fA-F0-9]{64}", // Private keys/hashes
"\\b[A-Za-z0-9+/]{20,}={0,2}\\b", // Base64 encoded data
}
for _, pattern := range sensitivePatterns {
if matched, _ := regexp.MatchString(pattern, text); matched {
return true
}
}
return false
}
// isSensitiveField checks if a field name indicates sensitive data
func (eh *ErrorHandler) isSensitiveField(fieldName string) bool {
return eh.sensitiveFields[strings.ToLower(fieldName)]
}
// updateMetrics updates error metrics
func (eh *ErrorHandler) updateMetrics(err *SecureError) {
eh.errorMetrics.TotalErrors++
eh.errorMetrics.ErrorsByCategory[err.Category]++
eh.errorMetrics.ErrorsBySeverity[err.Severity]++
if err.Sensitive {
eh.errorMetrics.SensitiveDataLeaks++
}
}
// logError logs the error appropriately based on sensitivity and severity
func (eh *ErrorHandler) logError(err *SecureError) {
logContext := map[string]interface{}{
"error_code": err.Code,
"error_category": string(err.Category),
"error_severity": string(err.Severity),
"timestamp": err.Timestamp,
}
// Add safe context
for key, value := range err.Context {
if !eh.isSensitiveField(key) {
logContext[key] = value
}
}
logMessage := err.Message
if err.Sensitive {
logMessage = "Sensitive error occurred (details redacted)"
logContext["sensitive"] = true
}
switch err.Severity {
case ErrorSeverityCritical:
eh.logger.Error(logMessage)
case ErrorSeverityHigh:
eh.logger.Error(logMessage)
case ErrorSeverityMedium:
eh.logger.Warn(logMessage)
case ErrorSeverityLow:
eh.logger.Info(logMessage)
default:
eh.logger.Info(logMessage)
}
}
// GetMetrics returns current error metrics
func (eh *ErrorHandler) GetMetrics() *ErrorMetrics {
return eh.errorMetrics
}
// Common error creation helpers
// NewAuthenticationError creates a new authentication error
func (eh *ErrorHandler) NewAuthenticationError(message string, err error) *SecureError {
return eh.WrapError(err, "AUTH_FAILED", message, ErrorCategoryAuthentication, ErrorSeverityHigh)
}
// NewAuthorizationError creates a new authorization error
func (eh *ErrorHandler) NewAuthorizationError(message string, err error) *SecureError {
return eh.WrapError(err, "AUTHZ_FAILED", message, ErrorCategoryAuthorization, ErrorSeverityHigh)
}
// NewValidationError creates a new validation error
func (eh *ErrorHandler) NewValidationError(message string, err error) *SecureError {
return eh.WrapError(err, "VALIDATION_FAILED", message, ErrorCategoryValidation, ErrorSeverityMedium)
}
// NewRateLimitError creates a new rate limit error
func (eh *ErrorHandler) NewRateLimitError(message string, err error) *SecureError {
return eh.WrapError(err, "RATE_LIMIT_EXCEEDED", message, ErrorCategoryRateLimit, ErrorSeverityMedium)
}
// NewEncryptionError creates a new encryption error
func (eh *ErrorHandler) NewEncryptionError(message string, err error) *SecureError {
return eh.WrapError(err, "ENCRYPTION_FAILED", message, ErrorCategoryEncryption, ErrorSeverityCritical)
}
// NewTransactionError creates a new transaction error
func (eh *ErrorHandler) NewTransactionError(message string, err error) *SecureError {
return eh.WrapError(err, "TRANSACTION_FAILED", message, ErrorCategoryTransaction, ErrorSeverityHigh)
}
// NewInternalError creates a new internal error
func (eh *ErrorHandler) NewInternalError(message string, err error) *SecureError {
return eh.WrapError(err, "INTERNAL_ERROR", message, ErrorCategoryInternal, ErrorSeverityCritical)
}