Files
summercms/internal/argmask/argmask.go

64 lines
1.9 KiB
Go

// Package argmask renders a command line for messages with the values of
// secret-named long flags masked; bonfire's wrong-directory hint and
// summer's delegate errors share it.
package argmask
import "strings"
// secretWords mark a long flag whose value is masked.
var secretWords = []string{"password", "passwd", "secret", "token", "key"}
// secretFlag reports whether the long flag name (without "--") looks like it
// carries a secret.
func secretFlag(name string) bool {
name = strings.ToLower(name)
for _, w := range secretWords {
if strings.Contains(name, w) {
return true
}
}
return false
}
// Join renders args as one line, separated by single spaces and each
// quoted for a shell (Quote). The value of a long flag named like a secret
// becomes ***, in the "--flag=value" form and in the "--flag value" form,
// so a password typed on the command line does not reach stderr and CI
// logs. In the "--flag value" form the next argument is masked whatever its
// first character (pflag takes "-Xk9" as the value too); only "--" is left
// alone. Join cannot see flag types, so a secret-named boolean flag
// over-masks the argument after it, which is the safe side.
func Join(args []string) string {
var b strings.Builder
for i := 0; i < len(args); i++ {
if i > 0 {
b.WriteByte(' ')
}
a := args[i]
name, long := strings.CutPrefix(a, "--")
flag, _, inline := strings.Cut(name, "=")
if !long || flag == "" || !secretFlag(flag) {
b.WriteString(Quote(a))
continue
}
if inline {
b.WriteString(Quote("--"+flag) + "=***")
continue
}
b.WriteString(Quote(a))
if i+1 < len(args) && args[i+1] != "--" {
b.WriteString(" ***")
i++
}
}
return b.String()
}
// Quote single-quotes s when a shell would split or expand it.
func Quote(s string) string {
if s != "" && !strings.ContainsAny(s, " \t\n'\"\\$`*?[]{}()<>|&;#~!") {
return s
}
return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'"
}