2026-01-02 17:40:57 +08:00
|
|
|
|
package logredact
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"encoding/json"
|
2026-02-09 09:58:13 +08:00
|
|
|
|
"regexp"
|
2026-01-02 17:40:57 +08:00
|
|
|
|
"strings"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// maxRedactDepth 限制递归深度以防止栈溢出
|
|
|
|
|
|
const maxRedactDepth = 32
|
|
|
|
|
|
|
|
|
|
|
|
var defaultSensitiveKeys = map[string]struct{}{
|
|
|
|
|
|
"authorization_code": {},
|
|
|
|
|
|
"code": {},
|
|
|
|
|
|
"code_verifier": {},
|
|
|
|
|
|
"access_token": {},
|
|
|
|
|
|
"refresh_token": {},
|
|
|
|
|
|
"id_token": {},
|
|
|
|
|
|
"client_secret": {},
|
|
|
|
|
|
"password": {},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-09 09:58:13 +08:00
|
|
|
|
var defaultSensitiveKeyList = []string{
|
|
|
|
|
|
"authorization_code",
|
|
|
|
|
|
"code",
|
|
|
|
|
|
"code_verifier",
|
|
|
|
|
|
"access_token",
|
|
|
|
|
|
"refresh_token",
|
|
|
|
|
|
"id_token",
|
|
|
|
|
|
"client_secret",
|
|
|
|
|
|
"password",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
|
reGOCSPX = regexp.MustCompile(`GOCSPX-[0-9A-Za-z_-]{24,}`)
|
|
|
|
|
|
reAIza = regexp.MustCompile(`AIza[0-9A-Za-z_-]{35}`)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-02 17:40:57 +08:00
|
|
|
|
func RedactMap(input map[string]any, extraKeys ...string) map[string]any {
|
|
|
|
|
|
if input == nil {
|
|
|
|
|
|
return map[string]any{}
|
|
|
|
|
|
}
|
|
|
|
|
|
keys := buildKeySet(extraKeys)
|
|
|
|
|
|
redacted, ok := redactValueWithDepth(input, keys, 0).(map[string]any)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return map[string]any{}
|
|
|
|
|
|
}
|
|
|
|
|
|
return redacted
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func RedactJSON(raw []byte, extraKeys ...string) string {
|
|
|
|
|
|
if len(raw) == 0 {
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
var value any
|
|
|
|
|
|
if err := json.Unmarshal(raw, &value); err != nil {
|
|
|
|
|
|
return "<non-json payload redacted>"
|
|
|
|
|
|
}
|
|
|
|
|
|
keys := buildKeySet(extraKeys)
|
|
|
|
|
|
redacted := redactValueWithDepth(value, keys, 0)
|
|
|
|
|
|
encoded, err := json.Marshal(redacted)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "<redacted>"
|
|
|
|
|
|
}
|
|
|
|
|
|
return string(encoded)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-09 09:58:13 +08:00
|
|
|
|
// RedactText 对非结构化文本做轻量脱敏。
|
|
|
|
|
|
//
|
|
|
|
|
|
// 规则:
|
|
|
|
|
|
// - 如果文本本身是 JSON,则按 RedactJSON 处理。
|
|
|
|
|
|
// - 否则尝试对常见 key=value / key:"value" 片段做脱敏。
|
|
|
|
|
|
//
|
|
|
|
|
|
// 注意:该函数用于日志/错误信息兜底,不保证覆盖所有格式。
|
|
|
|
|
|
func RedactText(input string, extraKeys ...string) string {
|
|
|
|
|
|
input = strings.TrimSpace(input)
|
|
|
|
|
|
if input == "" {
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
raw := []byte(input)
|
|
|
|
|
|
if json.Valid(raw) {
|
|
|
|
|
|
return RedactJSON(raw, extraKeys...)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
keyAlt := buildKeyAlternation(extraKeys)
|
|
|
|
|
|
// JSON-like: "access_token":"..."
|
|
|
|
|
|
reJSONLike := regexp.MustCompile(`(?i)("(?:` + keyAlt + `)"\s*:\s*")([^"]*)(")`)
|
|
|
|
|
|
// Query-like: access_token=...
|
|
|
|
|
|
reQueryLike := regexp.MustCompile(`(?i)\b((?:` + keyAlt + `))=([^&\s]+)`)
|
|
|
|
|
|
// Plain: access_token: ... / access_token = ...
|
|
|
|
|
|
rePlain := regexp.MustCompile(`(?i)\b((?:` + keyAlt + `))\b(\s*[:=]\s*)([^,\s]+)`)
|
|
|
|
|
|
|
|
|
|
|
|
out := input
|
|
|
|
|
|
out = reGOCSPX.ReplaceAllString(out, "GOCSPX-***")
|
|
|
|
|
|
out = reAIza.ReplaceAllString(out, "AIza***")
|
|
|
|
|
|
out = reJSONLike.ReplaceAllString(out, `$1***$3`)
|
|
|
|
|
|
out = reQueryLike.ReplaceAllString(out, `$1=***`)
|
|
|
|
|
|
out = rePlain.ReplaceAllString(out, `$1$2***`)
|
|
|
|
|
|
return out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func buildKeyAlternation(extraKeys []string) string {
|
|
|
|
|
|
seen := make(map[string]struct{}, len(defaultSensitiveKeyList)+len(extraKeys))
|
|
|
|
|
|
keys := make([]string, 0, len(defaultSensitiveKeyList)+len(extraKeys))
|
|
|
|
|
|
for _, k := range defaultSensitiveKeyList {
|
|
|
|
|
|
seen[k] = struct{}{}
|
|
|
|
|
|
keys = append(keys, regexp.QuoteMeta(k))
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, k := range extraKeys {
|
|
|
|
|
|
n := normalizeKey(k)
|
|
|
|
|
|
if n == "" {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if _, ok := seen[n]; ok {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
seen[n] = struct{}{}
|
|
|
|
|
|
keys = append(keys, regexp.QuoteMeta(n))
|
|
|
|
|
|
}
|
|
|
|
|
|
return strings.Join(keys, "|")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-02 17:40:57 +08:00
|
|
|
|
func buildKeySet(extraKeys []string) map[string]struct{} {
|
|
|
|
|
|
keys := make(map[string]struct{}, len(defaultSensitiveKeys)+len(extraKeys))
|
|
|
|
|
|
for k := range defaultSensitiveKeys {
|
|
|
|
|
|
keys[k] = struct{}{}
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, key := range extraKeys {
|
|
|
|
|
|
normalized := normalizeKey(key)
|
|
|
|
|
|
if normalized == "" {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
keys[normalized] = struct{}{}
|
|
|
|
|
|
}
|
|
|
|
|
|
return keys
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func redactValueWithDepth(value any, keys map[string]struct{}, depth int) any {
|
|
|
|
|
|
if depth > maxRedactDepth {
|
|
|
|
|
|
return "<depth limit exceeded>"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
switch v := value.(type) {
|
|
|
|
|
|
case map[string]any:
|
|
|
|
|
|
out := make(map[string]any, len(v))
|
|
|
|
|
|
for k, val := range v {
|
|
|
|
|
|
if isSensitiveKey(k, keys) {
|
|
|
|
|
|
out[k] = "***"
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
out[k] = redactValueWithDepth(val, keys, depth+1)
|
|
|
|
|
|
}
|
|
|
|
|
|
return out
|
|
|
|
|
|
case []any:
|
|
|
|
|
|
out := make([]any, len(v))
|
|
|
|
|
|
for i, item := range v {
|
|
|
|
|
|
out[i] = redactValueWithDepth(item, keys, depth+1)
|
|
|
|
|
|
}
|
|
|
|
|
|
return out
|
|
|
|
|
|
default:
|
|
|
|
|
|
return value
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func isSensitiveKey(key string, keys map[string]struct{}) bool {
|
|
|
|
|
|
_, ok := keys[normalizeKey(key)]
|
|
|
|
|
|
return ok
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func normalizeKey(key string) string {
|
|
|
|
|
|
return strings.ToLower(strings.TrimSpace(key))
|
|
|
|
|
|
}
|