Zachary Rice 3 лет назад
Родитель
Сommit
96eed6aa0f
12 измененных файлов с 134 добавлено и 15 удалено
  1. 13 0
      .gitleaksignore
  2. 11 2
      cmd/detect.go
  3. 1 2
      cmd/generate/config/rules/jwt.go
  4. 1 1
      cmd/protect.go
  5. 5 0
      detect/detect.go
  6. 15 3
      detect/detect_test.go
  7. 3 1
      detect/location.go
  8. 53 4
      detect/utils.go
  9. 10 0
      go.mod
  10. 17 0
      go.sum
  11. 4 1
      report/finding.go
  12. 1 1
      report/finding_test.go

+ 13 - 0
.gitleaksignore

@@ -713,3 +713,16 @@ testdata/expected/git/small-branch-foo.txt:aws-access-token:15
 testdata/expected/git/small.txt:aws-access-token:15
 testdata/expected/git/small.txt:aws-access-token:44
 testdata/repos/nogit/main.go:aws-access-token:20
+3df8c3deb7bc1e34210bdbce114f1c6165bc6ac8:detect/detect_test.go:aws-access-token:513
+3df8c3deb7bc1e34210bdbce114f1c6165bc6ac8:detect/detect_test.go:aws-access-token:492
+3df8c3deb7bc1e34210bdbce114f1c6165bc6ac8:detect/detect_test.go:aws-access-token:414
+3df8c3deb7bc1e34210bdbce114f1c6165bc6ac8:detect/detect_test.go:aws-access-token:388
+3df8c3deb7bc1e34210bdbce114f1c6165bc6ac8:detect/detect_test.go:aws-access-token:366
+3df8c3deb7bc1e34210bdbce114f1c6165bc6ac8:detect/detect_test.go:discord-api-token:255
+3df8c3deb7bc1e34210bdbce114f1c6165bc6ac8:detect/detect_test.go:discord-api-token:224
+3df8c3deb7bc1e34210bdbce114f1c6165bc6ac8:detect/detect_test.go:sidekiq-sensitive-url:177
+3df8c3deb7bc1e34210bdbce114f1c6165bc6ac8:detect/detect_test.go:sidekiq-secret:154
+3df8c3deb7bc1e34210bdbce114f1c6165bc6ac8:detect/detect_test.go:sidekiq-secret:130
+3df8c3deb7bc1e34210bdbce114f1c6165bc6ac8:detect/detect_test.go:aws-access-token:107
+3df8c3deb7bc1e34210bdbce114f1c6165bc6ac8:detect/detect_test.go:pypi-upload-token:84
+3df8c3deb7bc1e34210bdbce114f1c6165bc6ac8:detect/detect_test.go:aws-access-token:62

+ 11 - 2
cmd/detect.go

@@ -112,14 +112,14 @@ func runDetect(cmd *cobra.Command, args []string) {
 
 	// log info about the scan
 	if err == nil {
-		log.Info().Msgf("scan completed in %s", time.Since(start))
+		log.Info().Msgf("scan completed in %s", FormatDuration(time.Since(start)))
 		if len(findings) != 0 {
 			log.Warn().Msgf("leaks found: %d", len(findings))
 		} else {
 			log.Info().Msg("no leaks found")
 		}
 	} else {
-		log.Warn().Msgf("partial scan completed in %s", time.Since(start))
+		log.Warn().Msgf("partial scan completed in %s", FormatDuration(time.Since(start)))
 		if len(findings) != 0 {
 			log.Warn().Msgf("%d leaks found in partial scan", len(findings))
 		} else {
@@ -159,3 +159,12 @@ func fileExists(fileName string) bool {
 	}
 	return false
 }
+
+func FormatDuration(d time.Duration) string {
+	scale := 100 * time.Second
+	// look for the max scale that is smaller than d
+	for scale > d {
+		scale = scale / 10
+	}
+	return d.Round(scale / 100).String()
+}

+ 1 - 2
cmd/generate/config/rules/jwt.go

@@ -14,8 +14,7 @@ func JWT() *config.Rule {
 	}
 
 	// validate
-	tps := []string{`eyJhbGciOieeeiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwic3ViZSI6IjEyMzQ1Njc4OTAiLCJuYW1lZWEiOiJKb2huIERvZSIsInN1ZmV3YWZiIjoiMTIzNDU2Nzg5MCIsIm5hbWVmZWF3ZnciOiJKb2huIERvZSIsIm5hbWVhZmV3ZmEiOiJKb2huIERvZSIsInN1ZndhZndlYWIiOiIxMjM0NTY3ODkwIiwibmFtZWZ3YWYiOiJKb2huIERvZSIsInN1YmZ3YWYiOiIxMjM0NTY3ODkwIiwibmFtZndhZSI6IkpvaG4gRG9lIiwiaWZ3YWZhYXQiOjE1MTYyMzkwMjJ9.a_5icKBDo-8EjUlrfvz2k2k-FYaindQ0DEYNrlsnRG0
-    `, // gitleaks:allow
+	tps := []string{`eyJhbGciOieeeiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwic3ViZSI6IjEyMzQ1Njc4OTAiLCJuYW1lZWEiOiJKb2huIERvZSIsInN1ZmV3YWZiIjoiMTIzNDU2Nzg5MCIsIm5hbWVmZWF3ZnciOiJKb2huIERvZSIsIm5hbWVhZmV3ZmEiOiJKb2huIERvZSIsInN1ZndhZndlYWIiOiIxMjM0NTY3ODkwIiwibmFtZWZ3YWYiOiJKb2huIERvZSIsInN1YmZ3YWYiOiIxMjM0NTY3ODkwIiwibmFtZndhZSI6IkpvaG4gRG9lIiwiaWZ3YWZhYXQiOjE1MTYyMzkwMjJ9.a_5icKBDo-8EjUlrfvz2k2k-FYaindQ0DEYNrlsnRG0`, // gitleaks:allow
 		`JWT := eyJhbGciOieeeiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwic3ViZSI6IjEyMzQ1Njc4OTAiLCJuYW1lZWEiOiJKb2huIERvZSIsInN1ZmV3YWZiIjoiMTIzNDU2Nzg5MCIsIm5hbWVmZWF3ZnciOiJKb2huIERvZSIsIm5hbWVhZmV3ZmEiOiJKb2huIERvZSIsInN1ZndhZndlYWIiOiIxMjM0NTY3ODkwIiwibmFtZWZ3YWYiOiJKb2huIERvZSIsInN1YmZ3YWYiOiIxMjM0NTY3ODkwIiwibmFtZndhZSI6IkpvaG4gRG9lIiwiaWZ3YWZhYXQiOjE1MTYyMzkwMjJ9.a_5icKBDo-8EjUlrfvz2k2k-FYaindQ0DEYNrlsnRG0`, // gitleaks:allow
 	}
 	return validate(r, tps, nil)

+ 1 - 1
cmd/protect.go

@@ -85,7 +85,7 @@ func runProtect(cmd *cobra.Command, args []string) {
 	}
 
 	// log info about the scan
-	log.Info().Msgf("scan completed in %s", time.Since(start))
+	log.Info().Msgf("scan completed in %s", FormatDuration(time.Since(start)))
 	if len(findings) != 0 {
 		log.Warn().Msgf("leaks found: %d", len(findings))
 	} else {

+ 5 - 0
detect/detect.go

@@ -204,6 +204,10 @@ func (d *Detector) detectRule(fragment Fragment, rule config.Rule) []report.Find
 		// value is set for this rule
 		loc := location(fragment, matchIndex)
 
+		if matchIndex[1] > loc.endLineIndex {
+			loc.endLineIndex = matchIndex[1]
+		}
+
 		finding := report.Finding{
 			Description: rule.Description,
 			File:        fragment.FilePath,
@@ -215,6 +219,7 @@ func (d *Detector) detectRule(fragment Fragment, rule config.Rule) []report.Find
 			Secret:      secret,
 			Match:       secret,
 			Tags:        rule.Tags,
+			Line:        fragment.Raw[loc.startLineIndex:loc.endLineIndex],
 		}
 
 		if strings.Contains(fragment.Raw[loc.startLineIndex:loc.endLineIndex],

+ 15 - 3
detect/detect_test.go

@@ -36,10 +36,9 @@ func TestDetect(t *testing.T) {
 			fragment: Fragment{
 				Raw: `awsToken := \
 
-                \"AKIALALEMEL33243OKIA\ // gitleaks:allow"
+		        \"AKIALALEMEL33243OKIA\ // gitleaks:allow"
 
-
-                `,
+		        `,
 				FilePath: "tmp.go",
 			},
 			expectedFindings: []report.Finding{},
@@ -60,6 +59,7 @@ func TestDetect(t *testing.T) {
 					Secret:      "AKIALALEMEL33243OKIA",
 					Match:       "AKIALALEMEL33243OKIA",
 					File:        "tmp.go",
+					Line:        `awsToken := \"AKIALALEMEL33243OKIA\"`,
 					RuleID:      "aws-access-key",
 					Tags:        []string{"key", "AWS"},
 					StartLine:   0,
@@ -81,6 +81,7 @@ func TestDetect(t *testing.T) {
 					Description: "PyPI upload token",
 					Secret:      "pypi-AgEIcHlwaS5vcmcAAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAAB",
 					Match:       "pypi-AgEIcHlwaS5vcmcAAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAAB",
+					Line:        `pypi-AgEIcHlwaS5vcmcAAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAAB`,
 					File:        "tmp.go",
 					RuleID:      "pypi-upload-token",
 					Tags:        []string{"key", "pypi"},
@@ -103,6 +104,7 @@ func TestDetect(t *testing.T) {
 					Description: "AWS Access Key",
 					Secret:      "AKIALALEMEL33243OLIA",
 					Match:       "AKIALALEMEL33243OLIA",
+					Line:        `awsToken := \"AKIALALEMEL33243OLIA\"`,
 					File:        "tmp.go",
 					RuleID:      "aws-access-key",
 					Tags:        []string{"key", "AWS"},
@@ -125,6 +127,7 @@ func TestDetect(t *testing.T) {
 					Description: "Sidekiq Secret",
 					Match:       "BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;",
 					Secret:      "cafebabe:deadbeef",
+					Line:        `export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;`,
 					File:        "tmp.sh",
 					RuleID:      "sidekiq-secret",
 					Tags:        []string{},
@@ -148,6 +151,7 @@ func TestDetect(t *testing.T) {
 					Match:       "BUNDLE_ENTERPRISE__CONTRIBSYS__COM=\"cafebabe:deadbeef\"",
 					Secret:      "cafebabe:deadbeef",
 					File:        "tmp.sh",
+					Line:        `echo hello1; export BUNDLE_ENTERPRISE__CONTRIBSYS__COM="cafebabe:deadbeef" && echo hello2`,
 					RuleID:      "sidekiq-secret",
 					Tags:        []string{},
 					Entropy:     2.6098502,
@@ -170,6 +174,7 @@ func TestDetect(t *testing.T) {
 					Match:       "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:",
 					Secret:      "cafeb4b3:d3adb33f",
 					File:        "tmp.sh",
+					Line:        `url = "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:80/path?param1=true&param2=false#heading1"`,
 					RuleID:      "sidekiq-sensitive-url",
 					Tags:        []string{},
 					Entropy:     2.984234,
@@ -216,6 +221,7 @@ func TestDetect(t *testing.T) {
 					Description: "Discord API key",
 					Match:       "Discord_Public_Key = \"e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5\"",
 					Secret:      "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5",
+					Line:        `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
 					File:        "tmp.go",
 					RuleID:      "discord-api-key",
 					Tags:        []string{},
@@ -246,6 +252,7 @@ func TestDetect(t *testing.T) {
 					Description: "Generic API Key",
 					Match:       "Key = \"e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5\"",
 					Secret:      "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5",
+					Line:        `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
 					File:        "tmp.py",
 					RuleID:      "generic-api-key",
 					Tags:        []string{},
@@ -356,6 +363,7 @@ func TestFromGit(t *testing.T) {
 					EndLine:     20,
 					StartColumn: 19,
 					EndColumn:   38,
+					Line:        "\n    awsToken := \"AKIALALEMEL33243OLIA\"",
 					Secret:      "AKIALALEMEL33243OLIA",
 					Match:       "AKIALALEMEL33243OLIA",
 					File:        "main.go",
@@ -377,6 +385,7 @@ func TestFromGit(t *testing.T) {
 					EndColumn:   36,
 					Secret:      "AKIALALEMEL33243OLIA",
 					Match:       "AKIALALEMEL33243OLIA",
+					Line:        "\n\taws_token := \"AKIALALEMEL33243OLIA\"",
 					File:        "foo/foo.go",
 					Date:        "2021-11-02T23:48:06Z",
 					Commit:      "491504d5a31946ce75e22554cc34203d8e5ff3ca",
@@ -402,6 +411,7 @@ func TestFromGit(t *testing.T) {
 					StartColumn: 17,
 					EndColumn:   36,
 					Secret:      "AKIALALEMEL33243OLIA",
+					Line:        "\n\taws_token := \"AKIALALEMEL33243OLIA\"",
 					Match:       "AKIALALEMEL33243OLIA",
 					Date:        "2021-11-02T23:48:06Z",
 					File:        "foo/foo.go",
@@ -479,6 +489,7 @@ func TestFromFiles(t *testing.T) {
 					EndColumn:   35,
 					Match:       "AKIALALEMEL33243OLIA",
 					Secret:      "AKIALALEMEL33243OLIA",
+					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
 					File:        "../testdata/repos/nogit/main.go",
 					RuleID:      "aws-access-key",
 					Tags:        []string{"key", "AWS"},
@@ -499,6 +510,7 @@ func TestFromFiles(t *testing.T) {
 					EndColumn:   35,
 					Match:       "AKIALALEMEL33243OLIA",
 					Secret:      "AKIALALEMEL33243OLIA",
+					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
 					File:        "../testdata/repos/nogit/main.go",
 					RuleID:      "aws-access-key",
 					Tags:        []string{"key", "AWS"},

+ 3 - 1
detect/location.go

@@ -21,6 +21,9 @@ func location(fragment Fragment, matchIndex []int) Location {
 	start := matchIndex[0]
 	end := matchIndex[1]
 
+	// default startLineIndex to 0
+	location.startLineIndex = 0
+
 	for lineNum, pair := range fragment.newlineIndices {
 		_lineNum = lineNum
 		newLineByteIndex := pair[0]
@@ -48,7 +51,6 @@ func location(fragment Fragment, matchIndex []int) Location {
 		location.endColumn = (end - prevNewLine)
 		location.startLine = _lineNum + 1
 		location.endLine = _lineNum + 1
-		location.startLineIndex = start
 
 		// search for new line byte index
 		i := 0

+ 53 - 4
detect/utils.go

@@ -1,12 +1,14 @@
 package detect
 
 import (
-	"encoding/json"
+	// "encoding/json"
 	"fmt"
 	"math"
 	"strings"
 	"time"
 
+	"github.com/charmbracelet/lipgloss"
+
 	"github.com/zricethezav/gitleaks/v8/report"
 
 	"github.com/gitleaks/go-gitdiff/gitdiff"
@@ -90,9 +92,56 @@ func filter(findings []report.Finding, redact bool) []report.Finding {
 }
 
 func printFinding(f report.Finding) {
-	var b []byte
-	b, _ = json.MarshalIndent(f, "", "	")
-	fmt.Println(string(b))
+	// trim all whitespace and tabs from the line
+	f.Line = strings.TrimSpace(f.Line)
+	// trim all whitespace and tabs from the secret
+	f.Secret = strings.TrimSpace(f.Secret)
+	// trim all whitespace and tabs from the match
+	f.Match = strings.TrimSpace(f.Match)
+
+	matchInLineIDX := strings.Index(f.Line, f.Match)
+	secretInMatchIdx := strings.Index(f.Match, f.Secret)
+
+	start := f.Line[0:matchInLineIDX]
+	startMatchIdx := 0
+	if matchInLineIDX > 20 {
+		startMatchIdx = matchInLineIDX - 20
+		start = "..." + f.Line[startMatchIdx:matchInLineIDX]
+	}
+
+	matchBeginning := lipgloss.NewStyle().SetString(f.Match[0:secretInMatchIdx]).Foreground(lipgloss.Color("#f5d445"))
+	secret := lipgloss.NewStyle().SetString(f.Secret).
+		Bold(true).
+		Italic(true).
+		Foreground(lipgloss.Color("#f05c07"))
+	matchEnd := lipgloss.NewStyle().SetString(f.Match[secretInMatchIdx+len(f.Secret):]).Foreground(lipgloss.Color("#f5d445"))
+	lineEnd := f.Line[matchInLineIDX+len(f.Match):]
+	if len(f.Secret) > 100 {
+		secret = lipgloss.NewStyle().SetString(f.Secret[0:100] + "...").
+			Bold(true).
+			Italic(true).
+			Foreground(lipgloss.Color("#f05c07"))
+	}
+	if len(lineEnd) > 20 {
+		lineEnd = lineEnd[0:20] + "..."
+	}
+
+	finding := fmt.Sprintf("%s%s%s%s%s\n", strings.TrimPrefix(strings.TrimLeft(start, " "), "\n"), matchBeginning, secret, matchEnd, lineEnd)
+	fmt.Printf("%-8s %s", "Finding:", finding)
+	fmt.Printf("%-8s %s\n", "Secret:", secret)
+	fmt.Printf("%-8s %s\n", "RuleID:", f.RuleID)
+	fmt.Printf("%-8s %f\n", "Entropy:", f.Entropy)
+	fmt.Printf("%-8s %s\n", "File:", f.File)
+	fmt.Printf("%-8s %d\n", "Line:", f.StartLine)
+	if f.Commit == "" {
+		fmt.Println("")
+		return
+	}
+	fmt.Printf("%-8s %s\n", "Commit:", f.Commit)
+	fmt.Printf("%-8s %s\n", "Author:", f.Author)
+	fmt.Printf("%-8s %s\n", "Email:", f.Email)
+	fmt.Printf("%-8s %s\n", "Date:", f.Date)
+	fmt.Println("")
 }
 
 func containsDigit(s string) bool {

+ 10 - 0
go.mod

@@ -12,6 +12,16 @@ require (
 	github.com/stretchr/testify v1.7.0
 )
 
+require (
+	github.com/charmbracelet/lipgloss v0.5.0 // indirect
+	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
+	github.com/mattn/go-isatty v0.0.14 // indirect
+	github.com/mattn/go-runewidth v0.0.13 // indirect
+	github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68 // indirect
+	github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0 // indirect
+	github.com/rivo/uniseg v0.2.0 // indirect
+)
+
 require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/fsnotify/fsnotify v1.4.9 // indirect

+ 17 - 0
go.sum

@@ -46,6 +46,8 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj
 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
 github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8=
+github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@@ -183,12 +185,19 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
 github.com/lucasjones/reggen v0.0.0-20200904144131-37ba4fa293bb h1:w1g9wNDIE/pHSTmAaUhv4TZQuPBS6GV3mMz5hkgziIU=
 github.com/lucasjones/reggen v0.0.0-20200904144131-37ba4fa293bb/go.mod h1:5ELEyG+X8f+meRWHuqUOewBOhvHkl7M76pdGEansxW4=
 github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
 github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
 github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
+github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
+github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
 github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
 github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@@ -202,6 +211,10 @@ github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68 h1:y1p/ycavWjGT9FnmSjdbWUlLGvcxrY0Rw3ATltrxOhk=
+github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
+github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0 h1:STjmj0uFfRryL9fzRA/OupNppeAID6QJYPMavTL7jtY=
+github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ=
 github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
@@ -214,6 +227,9 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
@@ -414,6 +430,7 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211110154304-99a53858aa08 h1:WecRHqgE09JBkh/584XIE6PMz5KKE/vER4izNUi30AQ=
 golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

+ 4 - 1
report/finding.go

@@ -13,6 +13,8 @@ type Finding struct {
 	StartColumn int
 	EndColumn   int
 
+	Line string `json:"-"`
+
 	Match string
 
 	// Secret contains the full content of what is matched in
@@ -42,6 +44,7 @@ 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 = "REDACT"
+	f.Secret = "REDACTED"
 }

+ 1 - 1
report/finding_test.go

@@ -19,7 +19,7 @@ func TestRedact(t *testing.T) {
 	for _, test := range tests {
 		for _, f := range test.findings {
 			f.Redact()
-			if f.Secret != "REDACT" {
+			if f.Secret != "REDACTED" {
 				t.Error("redact not redacting: ", f.Secret)
 			}
 		}