fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
197
consent-service/internal/middleware/pii_redactor.go
Normal file
197
consent-service/internal/middleware/pii_redactor.go
Normal file
@@ -0,0 +1,197 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user