86 lines
2.9 KiB
Go
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")
|
|
}
|