To gitea and beyond, let's go(-yco)
This commit is contained in:
175
internal/database/secure_logger.go
Normal file
175
internal/database/secure_logger.go
Normal file
@@ -0,0 +1,175 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user