Files
pxmon/internal/cli/monitor_privacy.go
2026-06-16 21:52:10 +04:00

86 lines
2.9 KiB
Go

package cli
import (
"regexp"
"strings"
)
// Privacy mode redacts sensitive tokens (IPs, long secret-like strings,
// hostnames, MAC addresses, ssh key material) from rendered output. The
// replacement uses a shifted block pattern (▚▞) which preserves token
// length so the layout doesn't shift but makes the content obviously
// unreadable — think "frosted glass" rather than the usual `****`.
var (
privacyIPv4 = regexp.MustCompile(`\b(?:\d{1,3}\.){3}\d{1,3}\b`)
privacyIPv6 = regexp.MustCompile(`\b(?:[0-9a-fA-F]{1,4}:){2,}[0-9a-fA-F:]{0,}\b`)
privacyMAC = regexp.MustCompile(`\b(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}\b`)
privacyHost = regexp.MustCompile(`\b[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}(?:\.[a-zA-Z0-9\-]{1,63}){1,}\b`)
privacyHex = regexp.MustCompile(`\b[A-Fa-f0-9]{24,}\b`)
privacyB64 = regexp.MustCompile(`\b[A-Za-z0-9+/]{28,}={0,2}\b`)
privacyKey = regexp.MustCompile(`(?i)(password|passwd|token|secret|apikey|api[_-]?key|bearer|authorization)\s*[:=]\s*\S+`)
privacyUsrAt = regexp.MustCompile(`[A-Za-z0-9._-]+@[A-Za-z0-9.-]+`)
)
// privacyGlyphs is a small set of dense unicode shade characters. By
// cycling through them the redacted range looks like diffused noise
// rather than a flat mask.
var privacyGlyphs = []rune{'▚', '▞', '▓', '▒'}
// privacyMask returns a redaction string the same visual length as src.
func privacyMask(src string) string {
rs := []rune(src)
out := make([]rune, len(rs))
for i, r := range rs {
if r == ' ' || r == '\t' || r == '\n' {
out[i] = r
continue
}
out[i] = privacyGlyphs[i%len(privacyGlyphs)]
}
return string(out)
}
// privacyRedact scrubs every sensitive pattern from the given line. The
// function is intentionally line-scoped — callers apply it row by row so
// that multi-line ANSI layouts survive the substitution.
func privacyRedact(line string) string {
if line == "" {
return line
}
replace := func(re *regexp.Regexp, s string) string {
return re.ReplaceAllStringFunc(s, privacyMask)
}
// Order matters: scrub the longest/most specific patterns first so
// later passes don't hit already-masked text.
line = privacyKey.ReplaceAllStringFunc(line, func(m string) string {
// Keep the label (password/token/etc) but mask the value.
idx := strings.IndexAny(m, "=:")
if idx < 0 {
return privacyMask(m)
}
return m[:idx+1] + privacyMask(strings.TrimLeft(m[idx+1:], " "))
})
line = replace(privacyB64, line)
line = replace(privacyHex, line)
line = replace(privacyMAC, line)
line = replace(privacyIPv4, line)
line = replace(privacyIPv6, line)
line = replace(privacyUsrAt, line)
line = replace(privacyHost, line)
return line
}
// applyPrivacyMultiline redacts every line independently. Safe to call
// on ANSI-styled output — masks only the literal runs a regex matches.
func applyPrivacyMultiline(s string) string {
if s == "" {
return s
}
lines := strings.Split(s, "\n")
for i, ln := range lines {
lines[i] = privacyRedact(ln)
}
return strings.Join(lines, "\n")
}