Files
ai-operator/internal/audit/redaction/redactor.go
T

137 lines
3.6 KiB
Go

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:])
}