86 lines
2.5 KiB
Go
86 lines
2.5 KiB
Go
|
|
package logger
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"fmt"
|
||
|
|
"log/slog"
|
||
|
|
"regexp"
|
||
|
|
"sort"
|
||
|
|
"strings"
|
||
|
|
"sync"
|
||
|
|
)
|
||
|
|
|
||
|
|
var secretRegistry = struct {
|
||
|
|
sync.RWMutex
|
||
|
|
values map[string]struct{}
|
||
|
|
}{values: map[string]struct{}{}}
|
||
|
|
var bearerPattern = regexp.MustCompile(`(?i)Bearer\s+[^\s"'<>]+`)
|
||
|
|
var credentialPattern = regexp.MustCompile(`(?i)((?:api[_-]?key|token|authorization|cookie|password|secret)["']?\s*[:=]\s*["']?)[^"'\s,;}]+`)
|
||
|
|
|
||
|
|
func RegisterSecrets(values ...string) {
|
||
|
|
secretRegistry.Lock()
|
||
|
|
defer secretRegistry.Unlock()
|
||
|
|
for _, v := range values {
|
||
|
|
if v != "" {
|
||
|
|
secretRegistry.values[v] = struct{}{}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
func redact(s string) string {
|
||
|
|
secretRegistry.RLock()
|
||
|
|
values := make([]string, 0, len(secretRegistry.values))
|
||
|
|
for value := range secretRegistry.values {
|
||
|
|
values = append(values, value)
|
||
|
|
}
|
||
|
|
secretRegistry.RUnlock()
|
||
|
|
sort.Slice(values, func(i, j int) bool { return len(values[i]) > len(values[j]) })
|
||
|
|
for _, value := range values {
|
||
|
|
s = strings.ReplaceAll(s, value, "[redacted]")
|
||
|
|
}
|
||
|
|
s = bearerPattern.ReplaceAllString(s, "Bearer [redacted]")
|
||
|
|
return credentialPattern.ReplaceAllString(s, "${1}[redacted]")
|
||
|
|
}
|
||
|
|
|
||
|
|
type redactingHandler struct{ next slog.Handler }
|
||
|
|
|
||
|
|
func (h redactingHandler) Enabled(ctx context.Context, level slog.Level) bool {
|
||
|
|
return h.next.Enabled(ctx, level)
|
||
|
|
}
|
||
|
|
func redactAttr(a slog.Attr) slog.Attr {
|
||
|
|
key := strings.ToLower(a.Key)
|
||
|
|
for _, part := range []string{"apikey", "api_key", "token", "password", "secret", "authorization", "cookie", "payload", "headers", "body", "config"} {
|
||
|
|
if strings.Contains(key, part) {
|
||
|
|
return slog.String(a.Key, "[redacted]")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
v := a.Value.Resolve()
|
||
|
|
switch v.Kind() {
|
||
|
|
case slog.KindString:
|
||
|
|
a.Value = slog.StringValue(redact(v.String()))
|
||
|
|
case slog.KindAny:
|
||
|
|
a.Value = slog.StringValue(redact(fmt.Sprint(v.Any())))
|
||
|
|
case slog.KindGroup:
|
||
|
|
attrs := v.Group()
|
||
|
|
for i := range attrs {
|
||
|
|
attrs[i] = redactAttr(attrs[i])
|
||
|
|
}
|
||
|
|
a.Value = slog.GroupValue(attrs...)
|
||
|
|
}
|
||
|
|
return a
|
||
|
|
}
|
||
|
|
func (h redactingHandler) Handle(ctx context.Context, r slog.Record) error {
|
||
|
|
next := slog.NewRecord(r.Time, r.Level, redact(r.Message), r.PC)
|
||
|
|
r.Attrs(func(a slog.Attr) bool { next.AddAttrs(redactAttr(a)); return true })
|
||
|
|
return h.next.Handle(ctx, next)
|
||
|
|
}
|
||
|
|
func (h redactingHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||
|
|
copy := append([]slog.Attr(nil), attrs...)
|
||
|
|
for i := range copy {
|
||
|
|
copy[i] = redactAttr(copy[i])
|
||
|
|
}
|
||
|
|
return redactingHandler{h.next.WithAttrs(copy)}
|
||
|
|
}
|
||
|
|
func (h redactingHandler) WithGroup(name string) slog.Handler {
|
||
|
|
return redactingHandler{h.next.WithGroup(name)}
|
||
|
|
}
|