Skip to content

Commit

Permalink
feat: support masking secrets (#1115)
Browse files Browse the repository at this point in the history
* feat: support masking secrets

* fix: fix lint errors

* fix: fix error handling

* fix(mask): change the default separator from `;` to `,`

`;` has a special meaning in shell script.

For example, the following command doesn't work as expected.

```sh
export TFCMT_MASKS=env:GITHUB_TOKEN;env:DATADOG_API_KEY
```

To prevent the bug, I change the separator.
  • Loading branch information
suzuki-shunsuke committed Feb 1, 2024
1 parent b47d3c3 commit 74ffe99
Show file tree
Hide file tree
Showing 16 changed files with 178 additions and 13 deletions.
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -12,6 +12,7 @@ require (
github.com/suzuki-shunsuke/github-comment-metadata v0.1.0
github.com/suzuki-shunsuke/go-ci-env/v3 v3.0.1
github.com/suzuki-shunsuke/go-findconfig v1.2.0
github.com/suzuki-shunsuke/logrus-error v0.1.4
github.com/urfave/cli/v2 v2.27.1
golang.org/x/oauth2 v0.16.0
gopkg.in/yaml.v2 v2.4.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Expand Up @@ -66,6 +66,8 @@ github.com/suzuki-shunsuke/go-ci-env/v3 v3.0.1 h1:deCm9of48iwRq0Axg3ne8mx/26h3ok
github.com/suzuki-shunsuke/go-ci-env/v3 v3.0.1/go.mod h1:VmLj5u0w7Yf/IJIzZ+TWiB7mVT3pRKPMeb0Jssk7YsA=
github.com/suzuki-shunsuke/go-findconfig v1.2.0 h1:PWHIyKZEsVmZVh6+K+rHVw0/XjTFmQEYfa8ZIzIJd0c=
github.com/suzuki-shunsuke/go-findconfig v1.2.0/go.mod h1:lXzJUZQXrgsMmpHxXMVrWUAQpE4EopgDEJbwslvKbzs=
github.com/suzuki-shunsuke/logrus-error v0.1.4 h1:nWo98uba1fANHdZ9Y5pJ2RKs/PpVjrLzRp5m+mRb9KE=
github.com/suzuki-shunsuke/logrus-error v0.1.4/go.mod h1:WsVvvw6SKSt08/fB2qbnsKIMJA4K1MYCUprqsBJbMiM=
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
Expand Down
7 changes: 5 additions & 2 deletions pkg/apperr/error.go
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"

"github.com/sirupsen/logrus"
"github.com/suzuki-shunsuke/logrus-error/logerr"
)

// Exit codes are int values for the exit code that shell interpreter can interpret
Expand Down Expand Up @@ -58,17 +59,19 @@ func HandleExit(err error) int {
return ExitCodeOK
}

logE := logrus.NewEntry(logrus.New())

if exitErr, ok := err.(ExitCoder); ok { //nolint:errorlint
if err.Error() != "" {
if _, ok := exitErr.(ErrorFormatter); ok {
logrus.Errorf("%+v", err)
} else {
logrus.Error(err)
logerr.WithError(logE, err).Error("tfcmt failed")
}
}
return exitErr.ExitCode()
}

logrus.Error(err)
logerr.WithError(logE, err).Error("tfcmt failed")
return ExitCodeError
}
10 changes: 9 additions & 1 deletion pkg/cli/var.go
Expand Up @@ -5,6 +5,7 @@ import (
"strings"

"github.com/suzuki-shunsuke/tfcmt/v4/pkg/config"
"github.com/suzuki-shunsuke/tfcmt/v4/pkg/mask"
"github.com/urfave/cli/v2"
)

Expand Down Expand Up @@ -33,7 +34,7 @@ func parseVarEnvs(envs []string, m map[string]string) {
}
}

func parseOpts(ctx *cli.Context, cfg *config.Config, envs []string) error {
func parseOpts(ctx *cli.Context, cfg *config.Config, envs []string) error { //nolint:cyclop
if owner := ctx.String("owner"); owner != "" {
cfg.CI.Owner = owner
}
Expand Down Expand Up @@ -73,5 +74,12 @@ func parseOpts(ctx *cli.Context, cfg *config.Config, envs []string) error {
}
cfg.Vars = vm

// Mask https://github.com/suzuki-shunsuke/tfcmt/discussions/1083
masks, err := mask.ParseMasksFromEnv()
if err != nil {
return err
}
cfg.Masks = masks

return nil
}
20 changes: 14 additions & 6 deletions pkg/config/config.go
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"os"
"regexp"

"github.com/suzuki-shunsuke/go-findconfig/findconfig"
"gopkg.in/yaml.v2"
Expand All @@ -17,12 +18,19 @@ type Config struct {
EmbeddedVarNames []string `yaml:"embedded_var_names"`
Templates map[string]string
Log Log
GHEBaseURL string `yaml:"ghe_base_url"`
GHEGraphQLEndpoint string `yaml:"ghe_graphql_endpoint"`
PlanPatch bool `yaml:"plan_patch"`
RepoOwner string `yaml:"repo_owner"`
RepoName string `yaml:"repo_name"`
Output string `yaml:"-"`
GHEBaseURL string `yaml:"ghe_base_url"`
GHEGraphQLEndpoint string `yaml:"ghe_graphql_endpoint"`
PlanPatch bool `yaml:"plan_patch"`
RepoOwner string `yaml:"repo_owner"`
RepoName string `yaml:"repo_name"`
Output string `yaml:"-"`
Masks []*Mask `yaml:"-"`
}

type Mask struct {
Type string
Value string
Regexp *regexp.Regexp
}

type CI struct {
Expand Down
5 changes: 3 additions & 2 deletions pkg/controller/apply.go
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/mattn/go-colorable"
"github.com/suzuki-shunsuke/tfcmt/v4/pkg/apperr"
"github.com/suzuki-shunsuke/tfcmt/v4/pkg/mask"
"github.com/suzuki-shunsuke/tfcmt/v4/pkg/notifier"
"github.com/suzuki-shunsuke/tfcmt/v4/pkg/platform"
)
Expand Down Expand Up @@ -41,8 +42,8 @@ func (ctrl *Controller) Apply(ctx context.Context, command Command) error {
uncolorizedStdout := colorable.NewNonColorable(stdout)
uncolorizedStderr := colorable.NewNonColorable(stderr)
uncolorizedCombinedOutput := colorable.NewNonColorable(combinedOutput)
cmd.Stdout = io.MultiWriter(os.Stdout, uncolorizedStdout, uncolorizedCombinedOutput)
cmd.Stderr = io.MultiWriter(os.Stderr, uncolorizedStderr, uncolorizedCombinedOutput)
cmd.Stdout = io.MultiWriter(mask.NewWriter(os.Stdout, ctrl.Config.Masks), uncolorizedStdout, uncolorizedCombinedOutput)
cmd.Stderr = io.MultiWriter(mask.NewWriter(os.Stderr, ctrl.Config.Masks), uncolorizedStderr, uncolorizedCombinedOutput)
setCancel(cmd)
_ = cmd.Run()

Expand Down
2 changes: 2 additions & 0 deletions pkg/controller/controller.go
Expand Up @@ -143,6 +143,7 @@ func (ctrl *Controller) getNotifier(ctx context.Context) (notifier.Notifier, err
Vars: ctrl.Config.Vars,
EmbeddedVarNames: ctrl.Config.EmbeddedVarNames,
Templates: ctrl.Config.Templates,
Masks: ctrl.Config.Masks,
})
if err != nil {
return nil, err
Expand All @@ -169,6 +170,7 @@ func (ctrl *Controller) getNotifier(ctx context.Context) (notifier.Notifier, err
Templates: ctrl.Config.Templates,
Patch: ctrl.Config.PlanPatch,
SkipNoChanges: ctrl.Config.Terraform.Plan.WhenNoChanges.DisableComment,
Masks: ctrl.Config.Masks,
})
if err != nil {
return nil, err
Expand Down
5 changes: 3 additions & 2 deletions pkg/controller/plan.go
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/mattn/go-colorable"
"github.com/suzuki-shunsuke/tfcmt/v4/pkg/apperr"
"github.com/suzuki-shunsuke/tfcmt/v4/pkg/mask"
"github.com/suzuki-shunsuke/tfcmt/v4/pkg/notifier"
"github.com/suzuki-shunsuke/tfcmt/v4/pkg/platform"
)
Expand Down Expand Up @@ -42,8 +43,8 @@ func (ctrl *Controller) Plan(ctx context.Context, command Command) error {
uncolorizedStdout := colorable.NewNonColorable(stdout)
uncolorizedStderr := colorable.NewNonColorable(stderr)
uncolorizedCombinedOutput := colorable.NewNonColorable(combinedOutput)
cmd.Stdout = io.MultiWriter(os.Stdout, uncolorizedStdout, uncolorizedCombinedOutput)
cmd.Stderr = io.MultiWriter(os.Stderr, uncolorizedStderr, uncolorizedCombinedOutput)
cmd.Stdout = io.MultiWriter(mask.NewWriter(os.Stdout, ctrl.Config.Masks), uncolorizedStdout, uncolorizedCombinedOutput)
cmd.Stderr = io.MultiWriter(mask.NewWriter(os.Stderr, ctrl.Config.Masks), uncolorizedStderr, uncolorizedCombinedOutput)
setCancel(cmd)
_ = cmd.Run()

Expand Down
71 changes: 71 additions & 0 deletions pkg/mask/parser.go
@@ -0,0 +1,71 @@
package mask

import (
"errors"
"fmt"
"os"
"regexp"
"strings"

"github.com/sirupsen/logrus"
"github.com/suzuki-shunsuke/logrus-error/logerr"
"github.com/suzuki-shunsuke/tfcmt/v4/pkg/config"
)

func ParseMasksFromEnv() ([]*config.Mask, error) {
return ParseMasks(os.Getenv("TFCMT_MASKS"), os.Getenv("TFCMT_MASKS_SEPARATOR"))
}

func ParseMasks(maskStr, maskSep string) ([]*config.Mask, error) {
if maskStr == "" {
return nil, nil
}
if maskSep == "" {
maskSep = "," // default separator
}
maskStrs := strings.Split(maskStr, maskSep)
masks := make([]*config.Mask, 0, len(maskStrs))
for _, maskStr := range maskStrs {
mask, err := parseMask(maskStr)
if err != nil {
return nil, fmt.Errorf("parse a mask: %w", logerr.WithFields(err, logrus.Fields{
"mask": maskStr,
}))
}
if mask == nil {
continue
}
masks = append(masks, mask)
}
return masks, nil
}

func parseMask(maskStr string) (*config.Mask, error) {
typ, value, ok := strings.Cut(maskStr, ":")
if !ok {
return nil, errors.New("the mask is invalid. ':' is missing")
}
switch typ {
case "env":
if e := os.Getenv(value); e != "" {
return &config.Mask{
Type: "equal",
Value: e,
}, nil
}
// the environment variable is missing
return nil, nil //nolint:nilnil
case "regexp":
p, err := regexp.Compile(value)
if err != nil {
return nil, fmt.Errorf("the regular expression is invalid: %w", err)
}
return &config.Mask{
Type: "regexp",
Value: value,
Regexp: p,
}, nil
default:
return nil, errors.New("the mask type is invalid")
}
}
52 changes: 52 additions & 0 deletions pkg/mask/writer.go
@@ -0,0 +1,52 @@
package mask

import (
"io"
"strings"

"github.com/suzuki-shunsuke/tfcmt/v4/pkg/config"
)

const (
typeEqual = "equal"
typeRegexp = "regexp"
)

type Writer struct {
patterns []*config.Mask
w io.Writer
}

func NewWriter(w io.Writer, patterns []*config.Mask) *Writer {
return &Writer{
w: w,
patterns: patterns,
}
}

func (w *Writer) Write(p []byte) (int, error) {
a := p
for _, pattern := range w.patterns {
switch pattern.Type {
case typeEqual:
a = []byte(strings.ReplaceAll(string(a), pattern.Value, "***"))
case typeRegexp:
a = pattern.Regexp.ReplaceAll(a, []byte("***"))
}
}
_, err := w.w.Write(a)
return len(p), err
}

func Mask(s string, patterns []*config.Mask) string {
a := s
for _, pattern := range patterns {
switch pattern.Type {
case typeEqual:
a = strings.ReplaceAll(a, pattern.Value, "***")
case typeRegexp:
a = pattern.Regexp.ReplaceAllString(a, "***")
}
}
return a
}
3 changes: 3 additions & 0 deletions pkg/notifier/github/apply.go
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"

"github.com/sirupsen/logrus"
"github.com/suzuki-shunsuke/tfcmt/v4/pkg/mask"
"github.com/suzuki-shunsuke/tfcmt/v4/pkg/notifier"
"github.com/suzuki-shunsuke/tfcmt/v4/pkg/terraform"
)
Expand Down Expand Up @@ -74,6 +75,8 @@ func (g *NotifyService) Apply(ctx context.Context, param *notifier.ParamExec) er
// embed HTML tag to hide old comments
body += embeddedComment

body = mask.Mask(body, g.client.Config.Masks)

logE.Debug("create a comment")
if err := g.client.Comment.Post(ctx, body, &PostOptions{
Number: cfg.PR.Number,
Expand Down
2 changes: 2 additions & 0 deletions pkg/notifier/github/client.go
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/google/go-github/v58/github"
"github.com/shurcooL/githubv4"
"github.com/suzuki-shunsuke/tfcmt/v4/pkg/config"
"github.com/suzuki-shunsuke/tfcmt/v4/pkg/terraform"
"golang.org/x/oauth2"
)
Expand Down Expand Up @@ -53,6 +54,7 @@ type Config struct {
UseRawOutput bool
Patch bool
SkipNoChanges bool
Masks []*config.Mask
}

// PullRequest represents GitHub Pull Request metadata
Expand Down
3 changes: 3 additions & 0 deletions pkg/notifier/github/plan.go
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"

"github.com/sirupsen/logrus"
"github.com/suzuki-shunsuke/tfcmt/v4/pkg/mask"
"github.com/suzuki-shunsuke/tfcmt/v4/pkg/notifier"
"github.com/suzuki-shunsuke/tfcmt/v4/pkg/terraform"
)
Expand Down Expand Up @@ -80,6 +81,8 @@ func (g *NotifyService) Plan(ctx context.Context, param *notifier.ParamExec) err
// embed HTML tag to hide old comments
body += embeddedComment

body = mask.Mask(body, g.client.Config.Masks)

if cfg.Patch && cfg.PR.Number != 0 {
logE.Debug("try patching")
comments, err := g.client.Comment.List(ctx, cfg.Owner, cfg.Repo, cfg.PR.Number)
Expand Down
3 changes: 3 additions & 0 deletions pkg/notifier/localfile/apply.go
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"

"github.com/sirupsen/logrus"
"github.com/suzuki-shunsuke/tfcmt/v4/pkg/mask"
"github.com/suzuki-shunsuke/tfcmt/v4/pkg/notifier"
"github.com/suzuki-shunsuke/tfcmt/v4/pkg/terraform"
)
Expand Down Expand Up @@ -58,6 +59,8 @@ func (g *NotifyService) Apply(_ context.Context, param *notifier.ParamExec) erro
"program": "tfcmt",
})

body = mask.Mask(body, g.client.Config.Masks)

logE.Debug("writing the apply result to a file")
if err := g.client.Output.WriteToFile(body, cfg.OutputFile); err != nil {
return fmt.Errorf("write the apply result to a file: %w", err)
Expand Down
2 changes: 2 additions & 0 deletions pkg/notifier/localfile/client.go
@@ -1,6 +1,7 @@
package localfile

import (
"github.com/suzuki-shunsuke/tfcmt/v4/pkg/config"
"github.com/suzuki-shunsuke/tfcmt/v4/pkg/terraform"
)

Expand Down Expand Up @@ -28,6 +29,7 @@ type Config struct {
Templates map[string]string
CI string
UseRawOutput bool
Masks []*config.Mask
}

type service struct {
Expand Down
3 changes: 3 additions & 0 deletions pkg/notifier/localfile/plan.go
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"

"github.com/sirupsen/logrus"
"github.com/suzuki-shunsuke/tfcmt/v4/pkg/mask"
"github.com/suzuki-shunsuke/tfcmt/v4/pkg/notifier"
"github.com/suzuki-shunsuke/tfcmt/v4/pkg/terraform"
)
Expand Down Expand Up @@ -60,6 +61,8 @@ func (g *NotifyService) Plan(_ context.Context, param *notifier.ParamExec) error
"program": "tfcmt",
})

body = mask.Mask(body, g.client.Config.Masks)

logE.Debug("write a plan output to a file")
if err := g.client.Output.WriteToFile(body, cfg.OutputFile); err != nil {
return fmt.Errorf("write a plan output to a file: %w", err)
Expand Down

0 comments on commit 74ffe99

Please sign in to comment.