Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
113 lines
3.5 KiB
Go
113 lines
3.5 KiB
Go
// Package logredact strips PII and secrets from log strings before stdout/stderr.
|
|
package logredact
|
|
|
|
import (
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"regexp"
|
|
"sync"
|
|
)
|
|
|
|
const Redacted = "[REDACTED]"
|
|
|
|
var (
|
|
reAuthHeader = regexp.MustCompile(`(?i)\b(Bearer|Basic)\s+[A-Za-z0-9\-._~+/]+=*`)
|
|
reSecretAssign = regexp.MustCompile(`(?i)\b((?:api[_-]?key|access[_-]?token|secret(?:_key)?|password|passwd|authorization|credential|private[_-]?key)\s*[=:]\s*)["']?[^\s"',}]+["']?`)
|
|
reStripe = regexp.MustCompile(`\b(sk_live_|sk_test_|rk_live_|rk_test_|whsec_)[A-Za-z0-9]+`)
|
|
reOpenAI = regexp.MustCompile(`\bsk-[A-Za-z0-9]{20,}`)
|
|
reJWT = regexp.MustCompile(`\beyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}`)
|
|
reEmail = regexp.MustCompile(`\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b`)
|
|
reDSN = regexp.MustCompile(`(?i)\b((?:mysql|postgres|postgresql|redis|rediss|mongodb):\/\/)[^@\s]+@`)
|
|
|
|
reAuthPrefix = regexp.MustCompile(`(?i)^(Bearer|Basic)\s+`)
|
|
reAssignPrefix = regexp.MustCompile(`(?i)^((?:api[_-]?key|access[_-]?token|secret(?:_key)?|password|passwd|authorization|credential|private[_-]?key)\s*[=:]\s*)`)
|
|
)
|
|
|
|
// String redacts emails, tokens, Stripe/OpenAI keys, and DB URLs with credentials.
|
|
// Fail-safe: returns Redacted if redaction panics.
|
|
func String(input string) (out string) {
|
|
defer func() {
|
|
if recover() != nil {
|
|
out = Redacted
|
|
}
|
|
}()
|
|
out = input
|
|
out = reAuthHeader.ReplaceAllStringFunc(out, func(match string) string {
|
|
m := reAuthPrefix.FindStringSubmatch(match)
|
|
if m != nil {
|
|
return m[1] + " " + Redacted
|
|
}
|
|
return Redacted
|
|
})
|
|
out = reSecretAssign.ReplaceAllStringFunc(out, func(match string) string {
|
|
m := reAssignPrefix.FindStringSubmatch(match)
|
|
if m != nil {
|
|
return m[1] + Redacted
|
|
}
|
|
return Redacted
|
|
})
|
|
out = reStripe.ReplaceAllString(out, Redacted)
|
|
out = reOpenAI.ReplaceAllString(out, Redacted)
|
|
out = reJWT.ReplaceAllString(out, Redacted)
|
|
out = reEmail.ReplaceAllString(out, Redacted)
|
|
out = reDSN.ReplaceAllString(out, "${1}"+Redacted+"@")
|
|
return out
|
|
}
|
|
|
|
// ReplaceAttr is an slog.HandlerOptions.ReplaceAttr that redacts string attribute values and messages.
|
|
func ReplaceAttr(_ []string, a slog.Attr) slog.Attr {
|
|
switch a.Value.Kind() {
|
|
case slog.KindString:
|
|
a.Value = slog.StringValue(String(a.Value.String()))
|
|
case slog.KindAny:
|
|
if err, ok := a.Value.Any().(error); ok && err != nil {
|
|
a.Value = slog.StringValue(String(err.Error()))
|
|
}
|
|
}
|
|
if a.Key == slog.MessageKey && a.Value.Kind() == slog.KindString {
|
|
a.Value = slog.StringValue(String(a.Value.String()))
|
|
}
|
|
return a
|
|
}
|
|
|
|
// NewJSONHandler returns a JSON slog handler that redacts PII/secrets.
|
|
func NewJSONHandler(w io.Writer, opts *slog.HandlerOptions) slog.Handler {
|
|
if opts == nil {
|
|
opts = &slog.HandlerOptions{}
|
|
}
|
|
copied := *opts
|
|
prev := copied.ReplaceAttr
|
|
copied.ReplaceAttr = func(groups []string, a slog.Attr) slog.Attr {
|
|
if prev != nil {
|
|
a = prev(groups, a)
|
|
}
|
|
return ReplaceAttr(groups, a)
|
|
}
|
|
return slog.NewJSONHandler(w, &copied)
|
|
}
|
|
|
|
// Writer wraps an io.Writer so stdlib log output is redacted.
|
|
func Writer(w io.Writer) io.Writer {
|
|
if w == nil {
|
|
w = os.Stderr
|
|
}
|
|
return &redactWriter{w: w}
|
|
}
|
|
|
|
type redactWriter struct {
|
|
mu sync.Mutex
|
|
w io.Writer
|
|
}
|
|
|
|
func (r *redactWriter) Write(p []byte) (int, error) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
cleaned := String(string(p))
|
|
if _, err := r.w.Write([]byte(cleaned)); err != nil {
|
|
return 0, err
|
|
}
|
|
// Report original length so log.Logger does not retry/truncate oddly.
|
|
return len(p), nil
|
|
}
|