fetch_ml/internal/logging/sanitize.go
Jeremie Fraeys 412d7b82e9
security: implement comprehensive secrets protection
Critical fixes:
- Add SanitizeConnectionString() in storage/db_connect.go to remove passwords
- Add SecureEnvVar() in api/factory.go to clear env vars after reading (JWT_SECRET)
- Clear DB password from config after connection

Logging improvements:
- Enhance logging/sanitize.go with patterns for:
  - PostgreSQL connection strings
  - Generic connection string passwords
  - HTTP Authorization headers
  - Private keys

CLI security:
- Add --security-audit flag to api-server for security checks:
  - Config file permissions
  - Exposed environment variables
  - Running as root
  - API key file permissions
- Add warning when --api-key flag used (process list exposure)

Files changed:
- internal/storage/db_connect.go
- internal/api/factory.go
- internal/logging/sanitize.go
- internal/auth/flags.go
- cmd/api-server/main.go
2026-02-18 16:18:09 -05:00

107 lines
3.2 KiB
Go

package logging
import (
"regexp"
"strings"
)
// Patterns for sensitive data
var (
// API keys: 32+ hex characters
apiKeyPattern = regexp.MustCompile(`\b[0-9a-fA-F]{32,}\b`)
// JWT tokens
jwtPattern = regexp.MustCompile(`eyJ[a-zA-Z0-9_-]{10,}\.eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}`)
// Password-like fields in logs
passwordPattern = regexp.MustCompile(
`(?i)(password|passwd|pwd|secret|token|key)["']?\s*[:=]\s*["']?` +
`([^"'\s,}]+)`,
)
// Redis URLs with passwords
redisPasswordPattern = regexp.MustCompile(`redis://:[^@]+@`)
// Database connection strings - PostgreSQL
postgresPasswordPattern = regexp.MustCompile(`(postgresql?://[^:]+:)[^@]+(@)`)
// Generic connection strings with passwords
connStringPasswordPattern = regexp.MustCompile(`(password[=:])[^\s&;]+`)
// HTTP Authorization headers
authHeaderPattern = regexp.MustCompile(`(Authorization:\s*(?:Bearer|Basic)\s+)[^\s]+`)
// Private keys
privateKeyPattern = regexp.MustCompile(`(-----BEGIN (?:RSA|EC|DSA|OPENSSH) PRIVATE KEY-----)[\s\S]*?(-----END (?:RSA|EC|DSA|OPENSSH) PRIVATE KEY-----)`)
)
// SanitizeLogMessage removes sensitive data from log messages
func SanitizeLogMessage(message string) string {
// Redact API keys
message = apiKeyPattern.ReplaceAllString(message, "[REDACTED-API-KEY]")
// Redact JWT tokens
message = jwtPattern.ReplaceAllString(message, "[REDACTED-JWT]")
// Redact password-like fields
message = passwordPattern.ReplaceAllStringFunc(message, func(match string) string {
parts := passwordPattern.FindStringSubmatch(match)
if len(parts) >= 2 {
return parts[1] + "=[REDACTED]"
}
return match
})
// Redact Redis passwords from URLs
message = redisPasswordPattern.ReplaceAllString(message, "redis://:[REDACTED]@")
// Redact PostgreSQL connection strings
message = postgresPasswordPattern.ReplaceAllString(message, "${1}[REDACTED]${2}")
// Redact generic connection string passwords
message = connStringPasswordPattern.ReplaceAllString(message, "${1}[REDACTED]")
// Redact HTTP Authorization headers
message = authHeaderPattern.ReplaceAllString(message, "${1}[REDACTED]")
// Redact private keys
message = privateKeyPattern.ReplaceAllString(message, "[REDACTED-PRIVATE-KEY]")
return message
}
// SanitizeArgs removes sensitive data from structured log arguments
func SanitizeArgs(args []any) []any {
sanitized := make([]any, len(args))
copy(sanitized, args)
for i := 0; i < len(sanitized)-1; i += 2 {
// Check if this is a key-value pair
key, okKey := sanitized[i].(string)
value, okValue := sanitized[i+1].(string)
if okKey && okValue {
lowerKey := strings.ToLower(key)
// Redact sensitive fields
if strings.Contains(lowerKey, "password") ||
strings.Contains(lowerKey, "secret") ||
strings.Contains(lowerKey, "token") ||
strings.Contains(lowerKey, "key") ||
strings.Contains(lowerKey, "api") {
sanitized[i+1] = "[REDACTED]"
} else if strings.HasPrefix(value, "redis://") {
sanitized[i+1] = SanitizeLogMessage(value)
}
}
}
return sanitized
}
// RedactAPIKey masks an API key for logging (shows first/last 4 chars)
func RedactAPIKey(key string) string {
if len(key) <= 8 {
return "[REDACTED]"
}
return key[:4] + "..." + key[len(key)-4:]
}