package middleware import ( "regexp" "strings" ) // PIIPattern defines a pattern for identifying PII. type PIIPattern struct { Name string Pattern *regexp.Regexp Replacement string } // PIIRedactor redacts personally identifiable information from strings. type PIIRedactor struct { patterns []*PIIPattern } // Pre-compiled patterns for common PII types var ( emailPattern = regexp.MustCompile(`\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b`) ipv4Pattern = regexp.MustCompile(`\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b`) ipv6Pattern = regexp.MustCompile(`\b(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\b`) phonePattern = regexp.MustCompile(`(?:\+49|0049)[\s.-]?\d{2,4}[\s.-]?\d{3,8}|\b0\d{2,4}[\s.-]?\d{3,8}\b`) ibanPattern = regexp.MustCompile(`(?i)\b[A-Z]{2}\d{2}[\s]?(?:\d{4}[\s]?){3,5}\d{1,4}\b`) uuidPattern = regexp.MustCompile(`(?i)\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b`) namePattern = regexp.MustCompile(`\b(?:Herr|Frau|Hr\.|Fr\.)\s+[A-ZÄÖÜ][a-zäöüß]+(?:\s+[A-ZÄÖÜ][a-zäöüß]+)?\b`) ) // DefaultPIIPatterns returns the default set of PII patterns. func DefaultPIIPatterns() []*PIIPattern { return []*PIIPattern{ {Name: "email", Pattern: emailPattern, Replacement: "[EMAIL_REDACTED]"}, {Name: "ip_v4", Pattern: ipv4Pattern, Replacement: "[IP_REDACTED]"}, {Name: "ip_v6", Pattern: ipv6Pattern, Replacement: "[IP_REDACTED]"}, {Name: "phone", Pattern: phonePattern, Replacement: "[PHONE_REDACTED]"}, } } // AllPIIPatterns returns all available PII patterns. func AllPIIPatterns() []*PIIPattern { return []*PIIPattern{ {Name: "email", Pattern: emailPattern, Replacement: "[EMAIL_REDACTED]"}, {Name: "ip_v4", Pattern: ipv4Pattern, Replacement: "[IP_REDACTED]"}, {Name: "ip_v6", Pattern: ipv6Pattern, Replacement: "[IP_REDACTED]"}, {Name: "phone", Pattern: phonePattern, Replacement: "[PHONE_REDACTED]"}, {Name: "iban", Pattern: ibanPattern, Replacement: "[IBAN_REDACTED]"}, {Name: "uuid", Pattern: uuidPattern, Replacement: "[UUID_REDACTED]"}, {Name: "name", Pattern: namePattern, Replacement: "[NAME_REDACTED]"}, } } // NewPIIRedactor creates a new PII redactor with the given patterns. func NewPIIRedactor(patterns []*PIIPattern) *PIIRedactor { if patterns == nil { patterns = DefaultPIIPatterns() } return &PIIRedactor{patterns: patterns} } // NewDefaultPIIRedactor creates a PII redactor with default patterns. func NewDefaultPIIRedactor() *PIIRedactor { return NewPIIRedactor(DefaultPIIPatterns()) } // Redact removes PII from the given text. func (r *PIIRedactor) Redact(text string) string { if text == "" { return text } result := text for _, pattern := range r.patterns { result = pattern.Pattern.ReplaceAllString(result, pattern.Replacement) } return result } // ContainsPII checks if the text contains any PII. func (r *PIIRedactor) ContainsPII(text string) bool { if text == "" { return false } for _, pattern := range r.patterns { if pattern.Pattern.MatchString(text) { return true } } return false } // PIIFinding represents a found PII instance. type PIIFinding struct { Type string Match string Start int End int } // FindPII finds all PII in the text. func (r *PIIRedactor) FindPII(text string) []PIIFinding { if text == "" { return nil } var findings []PIIFinding for _, pattern := range r.patterns { matches := pattern.Pattern.FindAllStringIndex(text, -1) for _, match := range matches { findings = append(findings, PIIFinding{ Type: pattern.Name, Match: text[match[0]:match[1]], Start: match[0], End: match[1], }) } } return findings } // Default module-level redactor var defaultRedactor = NewDefaultPIIRedactor() // RedactPII is a convenience function that uses the default redactor. func RedactPII(text string) string { return defaultRedactor.Redact(text) } // ContainsPIIDefault checks if text contains PII using default patterns. func ContainsPIIDefault(text string) bool { return defaultRedactor.ContainsPII(text) } // RedactMap redacts PII from all string values in a map. func RedactMap(data map[string]interface{}) map[string]interface{} { result := make(map[string]interface{}) for key, value := range data { switch v := value.(type) { case string: result[key] = RedactPII(v) case map[string]interface{}: result[key] = RedactMap(v) case []interface{}: result[key] = redactSlice(v) default: result[key] = v } } return result } func redactSlice(data []interface{}) []interface{} { result := make([]interface{}, len(data)) for i, value := range data { switch v := value.(type) { case string: result[i] = RedactPII(v) case map[string]interface{}: result[i] = RedactMap(v) case []interface{}: result[i] = redactSlice(v) default: result[i] = v } } return result } // SafeLogString creates a safe-to-log version of sensitive data. // Use this for logging user-related information. func SafeLogString(format string, args ...interface{}) string { // Convert args to strings and redact safeArgs := make([]interface{}, len(args)) for i, arg := range args { switch v := arg.(type) { case string: safeArgs[i] = RedactPII(v) case error: safeArgs[i] = RedactPII(v.Error()) default: safeArgs[i] = arg } } // Note: We can't use fmt.Sprintf here due to the variadic nature // Instead, we redact the result result := format for _, arg := range safeArgs { if s, ok := arg.(string); ok { result = strings.Replace(result, "%s", s, 1) result = strings.Replace(result, "%v", s, 1) } } return RedactPII(result) }