89 lines
2.6 KiB
Go
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
|
|
}
|