176 lines
4.8 KiB
Go
176 lines
4.8 KiB
Go
package database
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"gorm.io/gorm/logger"
|
|
)
|
|
|
|
type SecureLogger struct {
|
|
writer logger.Writer
|
|
config logger.Config
|
|
sensitiveFields []string
|
|
sensitivePattern *regexp.Regexp
|
|
productionMode bool
|
|
}
|
|
|
|
func NewSecureLogger(writer logger.Writer, config logger.Config, productionMode bool) *SecureLogger {
|
|
sensitiveFields := []string{
|
|
"password", "token", "secret", "key", "hash", "salt",
|
|
"email_verification_token", "password_reset_token",
|
|
"token_hash", "jwt_secret", "api_key", "access_token",
|
|
"refresh_token", "session_id", "cookie", "auth",
|
|
}
|
|
|
|
sensitivePattern := regexp.MustCompile(`(?i)(password|token|secret|key|hash|salt|email_verification_token|password_reset_token|token_hash|jwt_secret|api_key|access_token|refresh_token|session_id|cookie|auth)`)
|
|
|
|
return &SecureLogger{
|
|
writer: writer,
|
|
config: config,
|
|
sensitiveFields: sensitiveFields,
|
|
sensitivePattern: sensitivePattern,
|
|
productionMode: productionMode,
|
|
}
|
|
}
|
|
|
|
func (l *SecureLogger) LogMode(level logger.LogLevel) logger.Interface {
|
|
newLogger := *l
|
|
newLogger.config.LogLevel = level
|
|
return &newLogger
|
|
}
|
|
|
|
func (l *SecureLogger) Info(ctx context.Context, msg string, data ...any) {
|
|
if l.config.LogLevel >= logger.Info {
|
|
l.log(ctx, "info", msg, data...)
|
|
}
|
|
}
|
|
|
|
func (l *SecureLogger) Warn(ctx context.Context, msg string, data ...any) {
|
|
if l.config.LogLevel >= logger.Warn {
|
|
l.log(ctx, "warn", msg, data...)
|
|
}
|
|
}
|
|
|
|
func (l *SecureLogger) Error(ctx context.Context, msg string, data ...any) {
|
|
if l.config.LogLevel >= logger.Error {
|
|
l.log(ctx, "error", msg, data...)
|
|
}
|
|
}
|
|
|
|
func (l *SecureLogger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
|
|
if l.config.LogLevel <= logger.Silent {
|
|
return
|
|
}
|
|
|
|
elapsed := time.Since(begin)
|
|
switch {
|
|
case err != nil && l.config.LogLevel >= logger.Error && (!l.config.IgnoreRecordNotFoundError || !IsRecordNotFoundError(err)):
|
|
sql, rows := fc()
|
|
l.log(ctx, "error", fmt.Sprintf("[%.3fms] [rows:%v] %s", float64(elapsed.Nanoseconds())/1e6, rows, sql))
|
|
case elapsed > l.config.SlowThreshold && l.config.SlowThreshold != 0 && l.config.LogLevel >= logger.Warn:
|
|
sql, rows := fc()
|
|
l.log(ctx, "warn", fmt.Sprintf("[%.3fms] [rows:%v] %s", float64(elapsed.Nanoseconds())/1e6, rows, sql))
|
|
case l.config.LogLevel == logger.Info:
|
|
sql, rows := fc()
|
|
l.log(ctx, "info", fmt.Sprintf("[%.3fms] [rows:%v] %s", float64(elapsed.Nanoseconds())/1e6, rows, sql))
|
|
}
|
|
}
|
|
|
|
func (l *SecureLogger) log(_ context.Context, level, msg string, data ...any) {
|
|
if l.productionMode {
|
|
msg = l.maskSensitiveData(msg)
|
|
|
|
maskedData := make([]any, len(data))
|
|
for i, d := range data {
|
|
maskedData[i] = l.maskSensitiveData(fmt.Sprintf("%v", d))
|
|
}
|
|
data = maskedData
|
|
}
|
|
|
|
formattedMsg := fmt.Sprintf(msg, data...)
|
|
|
|
l.writer.Printf("[%s] %s", strings.ToUpper(level), formattedMsg)
|
|
}
|
|
|
|
func (l *SecureLogger) maskSensitiveData(data string) string {
|
|
if l.productionMode {
|
|
data = regexp.MustCompile(`\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b`).ReplaceAllString(data, "[EMAIL_MASKED]")
|
|
|
|
data = regexp.MustCompile(`\b[A-Za-z0-9]{20,}\b`).ReplaceAllStringFunc(data, func(match string) string {
|
|
if l.sensitivePattern.MatchString(match) {
|
|
return "[TOKEN_MASKED]"
|
|
}
|
|
return match
|
|
})
|
|
|
|
data = l.maskSQLValues(data)
|
|
}
|
|
|
|
return data
|
|
}
|
|
|
|
func (l *SecureLogger) maskSQLValues(sql string) string {
|
|
paramPattern := regexp.MustCompile(`'([^']*)'`)
|
|
|
|
return paramPattern.ReplaceAllStringFunc(sql, func(match string) string {
|
|
value := strings.Trim(match, "'")
|
|
|
|
if l.isSensitiveValue(value) {
|
|
return "'[MASKED]'"
|
|
}
|
|
|
|
return match
|
|
})
|
|
}
|
|
|
|
func (l *SecureLogger) isSensitiveValue(value string) bool {
|
|
if regexp.MustCompile(`\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b`).MatchString(value) {
|
|
return true
|
|
}
|
|
|
|
if len(value) > 20 && regexp.MustCompile(`^[A-Za-z0-9+/]{20,}={0,2}$`).MatchString(value) {
|
|
return true
|
|
}
|
|
|
|
if regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`).MatchString(value) {
|
|
return true
|
|
}
|
|
|
|
if regexp.MustCompile(`^[A-Za-z0-9+/]+={0,2}$`).MatchString(value) && len(value) > 10 {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func IsRecordNotFoundError(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
return strings.Contains(strings.ToLower(err.Error()), "record not found") ||
|
|
strings.Contains(strings.ToLower(err.Error()), "not found")
|
|
}
|
|
|
|
func CreateSecureLogger(productionMode bool) logger.Interface {
|
|
config := logger.Config{
|
|
SlowThreshold: time.Second,
|
|
LogLevel: logger.Info,
|
|
IgnoreRecordNotFoundError: true,
|
|
Colorful: false,
|
|
}
|
|
|
|
if productionMode {
|
|
config.LogLevel = logger.Error
|
|
config.SlowThreshold = 2 * time.Second
|
|
}
|
|
|
|
writer := log.New(os.Stdout, "\r\n", log.LstdFlags)
|
|
return NewSecureLogger(writer, config, productionMode)
|
|
}
|