package redaction import ( "fmt" "strings" "unicode/utf8" ) type RedactionResult struct { Text string Applied bool Findings []Finding } type Finding struct { Type string Count int } type Redactor interface { RedactText(input string) RedactionResult RedactJSON(input map[string]any) map[string]any RedactBytesForLog(input []byte) string } type DefaultRedactor struct{} func New() DefaultRedactor { return DefaultRedactor{} } func (DefaultRedactor) RedactText(input string) RedactionResult { out := input counts := map[string]int{} apply := func(kind string, fn func(string) string) { before := out out = fn(out) if out != before { counts[kind]++ } } apply("database_url", func(s string) string { return databaseURLPattern.ReplaceAllString(s, `${1}:***MASKED***@`) }) apply("api_key", func(s string) string { return openAIKeyPattern.ReplaceAllString(s, `sk-***MASKED***`) }) apply("bearer", func(s string) string { return bearerPattern.ReplaceAllString(s, `Bearer ***MASKED***`) }) apply("secret", func(s string) string { return secretKVPattern.ReplaceAllString(s, `${1}=***MASKED***`) }) apply("email", func(s string) string { return emailPattern.ReplaceAllString(s, `${1}***${3}`) }) apply("otp", func(s string) string { return otpPattern.ReplaceAllStringFunc(s, func(m string) string { if strings.Contains(strings.ToLower(m), "услуг") { return m } parts := otpPattern.FindStringSubmatch(m) if len(parts) != 3 { return m } return parts[1] + "CODE" }) }) apply("account", func(s string) string { return accountPattern.ReplaceAllStringFunc(s, func(m string) string { parts := accountPattern.FindStringSubmatch(m) if len(parts) != 3 { return m } return parts[1] + maskMiddle(parts[2], 0, 4, "****") }) }) apply("phone", func(s string) string { return phonePattern.ReplaceAllStringFunc(s, func(m string) string { return maskMiddle(m, 4, 4, "***") }) }) apply("iin", func(s string) string { return digits12Pattern.ReplaceAllStringFunc(s, func(m string) string { return maskMiddle(m, 4, 4, "****") }) }) apply("card", func(s string) string { return digits16Pattern.ReplaceAllStringFunc(s, func(m string) string { return maskMiddle(m, 4, 4, "********") }) }) findings := make([]Finding, 0, len(counts)) for k, v := range counts { findings = append(findings, Finding{Type: k, Count: v}) } return RedactionResult{Text: out, Applied: out != input, Findings: findings} } func (r DefaultRedactor) RedactJSON(input map[string]any) map[string]any { return redactMap(r, input) } func (DefaultRedactor) RedactBytesForLog(input []byte) string { return fmt.Sprintf("[bytes:%d redacted]", len(input)) } func redactMap(r DefaultRedactor, input map[string]any) map[string]any { out := map[string]any{} for k, v := range input { out[k] = redactValue(r, v) } return out } func redactValue(r DefaultRedactor, v any) any { switch x := v.(type) { case string: return r.RedactText(x).Text case map[string]any: return redactMap(r, x) case []any: out := make([]any, len(x)) for i := range x { out[i] = redactValue(r, x[i]) } return out case []string: out := make([]string, len(x)) for i := range x { out[i] = r.RedactText(x[i]).Text } return out default: return v } } func maskMiddle(value string, keepStart, keepEnd int, mask string) string { if !utf8.ValidString(value) { return "***MASKED***" } r := []rune(value) if keepStart == 0 && keepEnd > 0 { if len(r) <= keepEnd { return mask } return mask + string(r[len(r)-keepEnd:]) } if len(r) <= keepStart+keepEnd { return strings.Repeat("*", len(r)) } return string(r[:keepStart]) + mask + string(r[len(r)-keepEnd:]) }