349 lines
10 KiB
Go
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)
|
|
}
|