// 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 }