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

feat: support masking secrets #1115

Merged
merged 4 commits into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
8 changes: 8 additions & 0 deletions pkg/cli/var.go
suzuki-shunsuke marked this conversation as resolved.
Show resolved Hide resolved
Expand Up @@ -5,6 +5,7 @@
"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 parseOpts(ctx *cli.Context, cfg *config.Config, envs []string) error {

Check failure on line 37 in pkg/cli/var.go

View workflow job for this annotation

GitHub Actions / test / test / test

error: calculated cyclomatic complexity for function parseOpts is 11, max is 10 (cyclop)
if owner := ctx.String("owner"); owner != "" {
cfg.CI.Owner = owner
}
Expand Down Expand Up @@ -73,5 +74,12 @@
}
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

Check failure on line 57 in pkg/mask/parser.go

View workflow job for this annotation

GitHub Actions / test / test / test

error: return both the `nil` error and invalid value: use a sentinel error instead (nilnil)
suzuki-shunsuke marked this conversation as resolved.
Show resolved Hide resolved
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")
}
}
54 changes: 54 additions & 0 deletions pkg/mask/writer.go
@@ -0,0 +1,54 @@
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) (n int, err error) {

Check failure on line 27 in pkg/mask/writer.go

View workflow job for this annotation

GitHub Actions / test / test / test

error: named return "n" with type "int" found (nonamedreturns)
suzuki-shunsuke marked this conversation as resolved.
Show resolved Hide resolved
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("***"))
}
}
if _, err := w.w.Write(a); err != nil {
return len(p), err
}
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(string(a), pattern.Value, "***")

Check failure on line 48 in pkg/mask/writer.go

View workflow job for this annotation

GitHub Actions / test / test / test

error: unnecessary conversion (unconvert)
suzuki-shunsuke marked this conversation as resolved.
Show resolved Hide resolved
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