Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement hide-secrets flag for run command #327

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 36 additions & 3 deletions pkg/cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"time"

Expand All @@ -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"
)

Expand Down Expand Up @@ -261,11 +264,41 @@ doppler run --mount secrets.json -- cat secrets.json`,
exitCode := 0
var err error

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 {
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 {
Expand Down Expand Up @@ -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 {
Expand Down
86 changes: 84 additions & 2 deletions pkg/utils/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ limitations under the License.
package utils

import (
"bytes"
"errors"
"fmt"
"io"
"os"
"os/exec"
"os/signal"
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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"}
Expand Down Expand Up @@ -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
}