From a955aa865bd031f5362948cf36fb55eba0d43106 Mon Sep 17 00:00:00 2001 From: remorses Date: Mon, 22 Aug 2022 18:48:32 +0200 Subject: [PATCH 1/2] init --- pkg/cmd/run.go | 39 +++++++++++++++++++-- pkg/utils/util.go | 86 +++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 120 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go index aab70c87..4f13610d 100644 --- a/pkg/cmd/run.go +++ b/pkg/cmd/run.go @@ -19,9 +19,11 @@ import ( "encoding/json" "errors" "fmt" + "io" "io/ioutil" "os" "path/filepath" + "sort" "strings" "time" @@ -33,6 +35,7 @@ import ( "github.com/DopplerHQ/cli/pkg/utils" "github.com/mattn/go-isatty" "github.com/spf13/cobra" + "golang.org/x/text/transform" "gopkg.in/gookit/color.v1" ) @@ -261,11 +264,41 @@ doppler run --mount secrets.json -- cat secrets.json`, exitCode := 0 var err error + sortedSecrets := make([]string, 0) + for _, secret := range secrets { + sortedSecrets = append(sortedSecrets, secret) + } + // if a longer secrets includes a shorter secret and i replace the shorter first, the longer secret won't be replaced in full + sort.Slice(sortedSecrets, func(i, j int) bool { + return len(sortedSecrets[i]) > len(sortedSecrets[j]) + }) + + transformers := make([]transform.Transformer, 0) + for _, secret := range sortedSecrets { + transformers = append( + transformers, + utils.BytesReplacer([]byte(secret), []byte(strings.Repeat("*", len(secret)))), + ) + } + + hideSecretsTransformer := transform.Chain(transformers...) + + var stdout io.Writer + var stderr io.Writer + + if utils.GetBoolFlag(cmd, "hide-secrets") { + stdout = transform.NewWriter(os.Stdout, hideSecretsTransformer) + stderr = transform.NewWriter(os.Stdout, hideSecretsTransformer) + } else { + stdout = os.Stdout + stderr = os.Stderr + } + if cmd.Flags().Changed("command") { command := cmd.Flag("command").Value.String() - exitCode, err = utils.RunCommandString(command, env, os.Stdin, os.Stdout, os.Stderr, forwardSignals, onExit) + exitCode, err = utils.RunCommandString(command, env, os.Stdin, stdout, stderr, forwardSignals, onExit) } else { - exitCode, err = utils.RunCommand(args, env, os.Stdin, os.Stdout, os.Stderr, forwardSignals, onExit) + exitCode, err = utils.RunCommand(args, env, os.Stdin, stdout, stderr, forwardSignals, onExit) } if err != nil { @@ -685,7 +718,7 @@ func init() { runCmd.Flags().String("mount-format", "json", fmt.Sprintf("file format to use. if not specified, will be auto-detected from mount name. one of %v", models.SecretsMountFormats)) runCmd.Flags().String("mount-template", "", "template file to use. secrets will be rendered into this template before mount. see 'doppler secrets substitute' for more info.") runCmd.Flags().Int("mount-max-reads", 0, "maximum number of times the mounted secrets file can be read (0 for unlimited)") - + runCmd.Flags().Bool("hide-secrets", false, "Replace secrets with '******' in the output") // deprecated runCmd.Flags().Bool("silent-exit", false, "disable error output if the supplied command exits non-zero") if err := runCmd.Flags().MarkDeprecated("silent-exit", "this behavior is now the default"); err != nil { diff --git a/pkg/utils/util.go b/pkg/utils/util.go index 8a733b2b..47773052 100644 --- a/pkg/utils/util.go +++ b/pkg/utils/util.go @@ -16,8 +16,10 @@ limitations under the License. package utils import ( + "bytes" "errors" "fmt" + "io" "os" "os/exec" "os/signal" @@ -34,6 +36,7 @@ import ( "github.com/atotto/clipboard" "github.com/google/uuid" "github.com/spf13/cobra" + "golang.org/x/text/transform" ) // ConfigDir DEPRECATED get configuration directory @@ -105,7 +108,7 @@ func Cwd() string { } // RunCommand runs the specified command -func RunCommand(command []string, env []string, inFile *os.File, outFile *os.File, errFile *os.File, forwardSignals bool, onExit func()) (int, error) { +func RunCommand(command []string, env []string, inFile *os.File, outFile io.Writer, errFile io.Writer, forwardSignals bool, onExit func()) (int, error) { cmd := exec.Command(command[0], command[1:]...) // #nosec G204 cmd.Env = env cmd.Stdin = inFile @@ -116,7 +119,7 @@ func RunCommand(command []string, env []string, inFile *os.File, outFile *os.Fil } // RunCommandString runs the specified command string -func RunCommandString(command string, env []string, inFile *os.File, outFile *os.File, errFile *os.File, forwardSignals bool, onExit func()) (int, error) { +func RunCommandString(command string, env []string, inFile *os.File, outFile io.Writer, errFile io.Writer, forwardSignals bool, onExit func()) (int, error) { shell := [2]string{"sh", "-c"} if IsWindows() { shell = [2]string{"cmd", "/C"} @@ -412,3 +415,82 @@ func RedactAuthToken(token string) string { return "[REDACTED]" } + +// ReplaceTransformer replaces text in a stream +// Taken from https://github.com/icholy/replace/blob/a7e12fe69d82503d82c3f85a9ca3973a11a2085f/replace.go#L12 +// See: http://golang.org/x/text/transform +type ReplaceTransformer struct { + transform.NopResetter + + old, new []byte + oldlen int +} + +var _ transform.Transformer = (*ReplaceTransformer)(nil) + +// BytesReplacer returns a transformer that replaces all instances of old with new. +// Unlike bytes.Replace, empty old values don't match anything. +func BytesReplacer(old, new []byte) ReplaceTransformer { + return ReplaceTransformer{old: old, new: new, oldlen: len(old)} +} + +// Transform implements golang.org/x/text/transform#Transformer +func (t ReplaceTransformer) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) { + var n int + // don't do anything for empty old string. We're forced to do this because an optimization in + // transform.String prevents us from generating any output when the src is empty. + // see: https://github.com/golang/text/blob/master/transform/transform.go#L570-L576 + if t.oldlen == 0 { + n, err = fullcopy(dst, src) + return n, n, err + } + // replace all instances of old with new + for { + i := bytes.Index(src[nSrc:], t.old) + if i == -1 { + break + } + // copy everything up to the match + n, err = fullcopy(dst[nDst:], src[nSrc:nSrc+i]) + nSrc += n + nDst += n + if err != nil { + return + } + // copy the new value + n, err = fullcopy(dst[nDst:], t.new) + if err != nil { + return + } + nDst += n + nSrc += t.oldlen + } + // if we're at the end, tack on any remaining bytes + if atEOF { + n, err = fullcopy(dst[nDst:], src[nSrc:]) + nDst += n + nSrc += n + return + } + // skip everything except the trailing len(r.old) - 1 + // we do this because there could be a match straddling + // the boundary + if skip := len(src[nSrc:]) - t.oldlen + 1; skip > 0 { + n, err = fullcopy(dst[nDst:], src[nSrc:nSrc+skip]) + nSrc += n + nDst += n + if err != nil { + return + } + } + err = transform.ErrShortSrc + return +} + +func fullcopy(dst, src []byte) (n int, err error) { + n = copy(dst, src) + if n < len(src) { + err = transform.ErrShortDst + } + return +} From 9790f16648e0f70ef16eb07e7144a7cf7a53b582 Mon Sep 17 00:00:00 2001 From: remorses Date: Mon, 22 Aug 2022 18:52:03 +0200 Subject: [PATCH 2/2] create hideSecretsTransformer only if flag is present --- pkg/cmd/run.go | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go index 4f13610d..18663d15 100644 --- a/pkg/cmd/run.go +++ b/pkg/cmd/run.go @@ -264,29 +264,29 @@ doppler run --mount secrets.json -- cat secrets.json`, exitCode := 0 var err error - sortedSecrets := make([]string, 0) - for _, secret := range secrets { - sortedSecrets = append(sortedSecrets, secret) - } - // if a longer secrets includes a shorter secret and i replace the shorter first, the longer secret won't be replaced in full - sort.Slice(sortedSecrets, func(i, j int) bool { - return len(sortedSecrets[i]) > len(sortedSecrets[j]) - }) - - transformers := make([]transform.Transformer, 0) - for _, secret := range sortedSecrets { - transformers = append( - transformers, - utils.BytesReplacer([]byte(secret), []byte(strings.Repeat("*", len(secret)))), - ) - } - - hideSecretsTransformer := transform.Chain(transformers...) - var stdout io.Writer var stderr io.Writer + // replace secrets with *** in command stdout and stderr if utils.GetBoolFlag(cmd, "hide-secrets") { + sortedSecrets := make([]string, 0) + for _, secret := range secrets { + sortedSecrets = append(sortedSecrets, secret) + } + // if a longer secret includes a shorter secret and i replace the shorter first, the longer secret won't be replaced in full + sort.Slice(sortedSecrets, func(i, j int) bool { + return len(sortedSecrets[i]) > len(sortedSecrets[j]) + }) + + transformers := make([]transform.Transformer, 0) + for _, secret := range sortedSecrets { + transformers = append( + transformers, + utils.BytesReplacer([]byte(secret), []byte(strings.Repeat("*", len(secret)))), + ) + } + + hideSecretsTransformer := transform.Chain(transformers...) stdout = transform.NewWriter(os.Stdout, hideSecretsTransformer) stderr = transform.NewWriter(os.Stdout, hideSecretsTransformer) } else {