Files
ThothII/tools/thothctl/internal/output/sanitize.go
T

89 lines
2.6 KiB
Go

// Package output removes credentials from diagnostics before they reach an operator terminal.
package output
import (
"errors"
"regexp"
"sort"
"strings"
"github.com/aritmolab/thothii/tools/thothctl/internal/safeio"
)
var credentialField = regexp.MustCompile(`(?im)(\b[\w.-]*(?:password|token|key)[\w.-]*\s*[:=]\s*)(?:"[^"\r\n]*"|'[^'\r\n]*'|[^\s,;]+)`)
const maxSecretFileBytes = 64 * 1024
const maxSecretSourceFiles = 32
const maxSecretSourceBytes = 256 * 1024
const maxDiagnosticDetailBytes = 512
// Sanitize redacts common credential fields and every supplied secret value.
func Sanitize(text string, secretValues []string) string {
text = credentialField.ReplaceAllString(text, "${1}[REDACTED]")
values := append([]string(nil), secretValues...)
sort.Slice(values, func(i, j int) bool { return len(values[i]) > len(values[j]) })
for _, value := range values {
if value != "" {
text = strings.ReplaceAll(text, value, "[REDACTED]")
}
}
return text
}
// SanitizeDetail redacts the complete subprocess detail before normalizing and bounding the text
// that may be displayed at the CLI boundary.
func SanitizeDetail(text string, secretValues []string) string {
detail := strings.Join(strings.Fields(Sanitize(text, secretValues)), " ")
if len(detail) <= maxDiagnosticDetailBytes {
return detail
}
var bounded strings.Builder
for _, character := range detail {
encoded := string(character)
if bounded.Len()+len(encoded) > maxDiagnosticDetailBytes {
break
}
bounded.WriteString(encoded)
}
return bounded.String()
}
// SecretValuesFromFiles reads non-empty secret-file contents without exposing them to callers.
func SecretValuesFromFiles(paths []string) ([]string, error) {
if len(paths) > maxSecretSourceFiles {
return nil, errors.New("declared secret file could not be read")
}
values := make([]string, 0, len(paths))
seen := make(map[string]struct{})
var totalBytes int64
for _, path := range paths {
value, size, err := readSecretFile(path)
if err != nil {
return nil, err
}
totalBytes += size
if totalBytes > maxSecretSourceBytes {
return nil, errors.New("declared secret file could not be read")
}
if value != "" {
if _, exists := seen[value]; exists {
continue
}
values = append(values, value)
seen[value] = struct{}{}
}
}
return values, nil
}
func readSecretFile(path string) (string, int64, error) {
contents, err := safeio.ReadCanonicalRegular(path, maxSecretFileBytes)
if err != nil {
return "", 0, errors.New("declared secret file could not be read")
}
return strings.TrimRight(string(contents), "\r\n"), int64(len(contents)), nil
}