Просмотр исходного кода

feat: Add optional redaction value, default 100 (#1229)

This commit introduces an optional value for the --redact flag, allowing
users to specify the extent of redaction for secrets. The valid range of
values is from 0 (no redaction) to 100 (full redaction).

If no value is provided, the default redaction value is set to 100,
resulting in complete redaction by replacing the secret with the term
'REDACTED'. This preserves the original behavior of the --redact flag.

Co-authored-by: Zachary Rice <zachary.rice@trufflesec.com>
Frank 2 лет назад
Родитель
Сommit
30c6117aaa
7 измененных файлов с 97 добавлено и 14 удалено
  1. 1 1
      cmd/detect.go
  2. 1 1
      cmd/protect.go
  3. 2 1
      cmd/root.go
  4. 1 1
      detect/detect.go
  5. 3 3
      detect/utils.go
  6. 23 4
      report/finding.go
  7. 66 3
      report/finding_test.go

+ 1 - 1
cmd/detect.go

@@ -72,7 +72,7 @@ func runDetect(cmd *cobra.Command, args []string) {
 		log.Fatal().Err(err).Msg("")
 	}
 	// set redact flag
-	if detector.Redact, err = cmd.Flags().GetBool("redact"); err != nil {
+	if detector.Redact, err = cmd.Flags().GetUint("redact"); err != nil {
 		log.Fatal().Err(err).Msg("")
 	}
 	if detector.MaxTargetMegaBytes, err = cmd.Flags().GetInt("max-target-megabytes"); err != nil {

+ 1 - 1
cmd/protect.go

@@ -64,7 +64,7 @@ func runProtect(cmd *cobra.Command, args []string) {
 		log.Fatal().Err(err).Msg("")
 	}
 	// set redact flag
-	if detector.Redact, err = cmd.Flags().GetBool("redact"); err != nil {
+	if detector.Redact, err = cmd.Flags().GetUint("redact"); err != nil {
 		log.Fatal().Err(err).Msg("")
 	}
 	if detector.MaxTargetMegaBytes, err = cmd.Flags().GetInt("max-target-megabytes"); err != nil {

+ 2 - 1
cmd/root.go

@@ -47,7 +47,8 @@ func init() {
 	rootCmd.PersistentFlags().BoolP("verbose", "v", false, "show verbose output from scan")
 	rootCmd.PersistentFlags().BoolP("no-color", "", false, "turn off color for verbose output")
 	rootCmd.PersistentFlags().Int("max-target-megabytes", 0, "files larger than this will be skipped")
-	rootCmd.PersistentFlags().Bool("redact", false, "redact secrets from logs and stdout")
+	rootCmd.PersistentFlags().Uint("redact", 0, "redact secrets from logs and stdout. To redact only parts of the secret just apply a percent value from 0..100. For example --redact=20 (default 100%)")
+	rootCmd.Flag("redact").NoOptDefVal = "100"
 	rootCmd.PersistentFlags().Bool("no-banner", false, "suppress banner")
 	err := viper.BindPFlag("config", rootCmd.PersistentFlags().Lookup("config"))
 	if err != nil {

+ 1 - 1
detect/detect.go

@@ -46,7 +46,7 @@ type Detector struct {
 	// Redact is a flag to redact findings. This is exported
 	// so users using gitleaks as a library can set this flag
 	// without calling `detector.Start(cmd *cobra.Command)`
-	Redact bool
+	Redact uint
 
 	// verbose is a flag to print findings
 	Verbose bool

+ 3 - 3
detect/utils.go

@@ -60,7 +60,7 @@ func shannonEntropy(data string) (entropy float64) {
 }
 
 // filter will dedupe and redact findings
-func filter(findings []report.Finding, redact bool) []report.Finding {
+func filter(findings []report.Finding, redact uint) []report.Finding {
 	var retFindings []report.Finding
 	for _, f := range findings {
 		include := true
@@ -81,8 +81,8 @@ func filter(findings []report.Finding, redact bool) []report.Finding {
 			}
 		}
 
-		if redact {
-			f.Redact()
+		if redact > 0 {
+			f.Redact(redact)
 		}
 		if include {
 			retFindings = append(retFindings, f)

+ 23 - 4
report/finding.go

@@ -1,6 +1,7 @@
 package report
 
 import (
+	"math"
 	"strings"
 )
 
@@ -43,8 +44,26 @@ type Finding struct {
 }
 
 // Redact removes sensitive information from a finding.
-func (f *Finding) Redact() {
-	f.Line = strings.Replace(f.Line, f.Secret, "REDACTED", -1)
-	f.Match = strings.Replace(f.Match, f.Secret, "REDACTED", -1)
-	f.Secret = "REDACTED"
+func (f *Finding) Redact(percent uint) {
+	secret := maskSecret(f.Secret, percent)
+	if percent >= 100 {
+		secret = "REDACTED"
+	}
+	f.Line = strings.Replace(f.Line, f.Secret, secret, -1)
+	f.Match = strings.Replace(f.Match, f.Secret, secret, -1)
+	f.Secret = secret
+}
+
+func maskSecret(secret string, percent uint) string {
+	if percent > 100 {
+		percent = 100
+	}
+	len := float64(len(secret))
+	if len <= 0 {
+		return secret
+	}
+	prc := float64(100 - percent)
+	lth := int64(math.RoundToEven(len * prc / float64(100)))
+
+	return secret[:lth] + "..."
 }

+ 66 - 3
report/finding_test.go

@@ -11,17 +11,80 @@ func TestRedact(t *testing.T) {
 			redact: true,
 			findings: []Finding{
 				{
-					Secret: "line containing secret",
-					Match:  "secret",
+					Match:  "line containing secret",
+					Secret: "secret",
 				},
 			}},
 	}
 	for _, test := range tests {
 		for _, f := range test.findings {
-			f.Redact()
+			f.Redact(100)
 			if f.Secret != "REDACTED" {
 				t.Error("redact not redacting: ", f.Secret)
 			}
+			if f.Match != "line containing REDACTED" {
+				t.Error("redact not redacting: ", f.Secret)
+			}
 		}
 	}
 }
+
+func TestMask(t *testing.T) {
+
+	tests := map[string]struct {
+		finding Finding
+		percent uint
+		expect  Finding
+	}{
+		"normal secret": {
+			finding: Finding{Match: "line containing secret", Secret: "secret"},
+			expect:  Finding{Match: "line containing se...", Secret: "se..."},
+			percent: 75,
+		},
+		"empty secret": {
+			finding: Finding{Match: "line containing", Secret: ""},
+			expect:  Finding{Match: "line containing", Secret: ""},
+			percent: 75,
+		},
+		"short secret": {
+			finding: Finding{Match: "line containing", Secret: "ss"},
+			expect:  Finding{Match: "line containing", Secret: "..."},
+			percent: 75,
+		},
+	}
+	for name, test := range tests {
+		t.Run(name, func(t *testing.T) {
+			f := test.finding
+			e := test.expect
+			f.Redact(test.percent)
+			if f.Secret != e.Secret {
+				t.Error("redact not redacting: ", f.Secret)
+			}
+			if f.Match != e.Match {
+				t.Error("redact not redacting: ", f.Match)
+			}
+		})
+	}
+}
+
+func TestMaskSecret(t *testing.T) {
+
+	tests := map[string]struct {
+		secret  string
+		percent uint
+		expect  string
+	}{
+		"normal masking":  {secret: "secret", percent: 75, expect: "se..."},
+		"high masking":    {secret: "secret", percent: 90, expect: "s..."},
+		"low masking":     {secret: "secret", percent: 10, expect: "secre..."},
+		"invalid masking": {secret: "secret", percent: 1000, expect: "..."},
+	}
+	for name, test := range tests {
+		t.Run(name, func(t *testing.T) {
+			got := maskSecret(test.secret, test.percent)
+			if got != test.expect {
+				t.Error("redact not redacting: ", got)
+			}
+		})
+	}
+}