Skip to content

Commit 45fc0af

Browse files
committed
fix
1 parent f2064c0 commit 45fc0af

File tree

3 files changed

+114
-38
lines changed

3 files changed

+114
-38
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,4 @@ Thumbs.db
3232
# Build artifacts
3333
dist/
3434
build/
35+
/.gosecretscanner/models

main.go

Lines changed: 31 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,13 @@ var (
9191
bracePlaceholderPattern = regexp.MustCompile(`\$\{[^}]+\}`)
9292
percentPlaceholderPattern = regexp.MustCompile(`%[A-Za-z0-9_]+%`)
9393
dollarPlaceholderPattern = regexp.MustCompile(`\$[A-Z0-9_]+`)
94+
95+
// Pre-compiled pattern for detecting regex definition lines (to skip self-matches)
96+
regexLinePattern = regexp.MustCompile("^\\s*`.*(?:\\(\\?i\\)|\\\\s|\\\\d|\\[|\\]|\\{|\\}|\\||\\^|\\$).*`")
9497
)
9598

99+
const ruleIDFmt = "pattern-%d"
100+
96101
type Secret struct {
97102
File string `json:"-"`
98103
FilePath string `json:"file"`
@@ -626,7 +631,7 @@ func scanFileForSecrets(path string, pipeline *verification.Pipeline) ([]Secret,
626631
LineNumber: lineNumber,
627632
Line: line,
628633
Match: matchedSecret,
629-
RuleID: fmt.Sprintf("pattern-%d", index),
634+
RuleID: fmt.Sprintf(ruleIDFmt, index),
630635
Type: secretPatterns[index],
631636
Confidence: confidence,
632637
Entropy: entropy,
@@ -643,7 +648,7 @@ func scanFileForSecrets(path string, pipeline *verification.Pipeline) ([]Secret,
643648
LineNumber: lineNumber,
644649
Line: line,
645650
Match: matchedSecret,
646-
RuleID: fmt.Sprintf("pattern-%d", index),
651+
RuleID: fmt.Sprintf(ruleIDFmt, index),
647652
Type: secretPatterns[index],
648653
Confidence: confidence,
649654
Entropy: entropy,
@@ -659,7 +664,7 @@ func scanFileForSecrets(path string, pipeline *verification.Pipeline) ([]Secret,
659664
LineNumber: lineNumber,
660665
Line: line,
661666
Match: matchedSecret,
662-
RuleID: fmt.Sprintf("pattern-%d", index),
667+
RuleID: fmt.Sprintf(ruleIDFmt, index),
663668
Type: secretPatterns[index],
664669
Confidence: confidence,
665670
Entropy: entropy,
@@ -679,49 +684,31 @@ func scanFileForSecrets(path string, pipeline *verification.Pipeline) ([]Secret,
679684
}
680685

681686
func AdditionalSecretPatterns() []string {
682-
vulnerabilityPatterns := []string{
683-
// Add your additional regex patterns here
684-
`(?i)(<\s*script\b[^>]*>(.*?)<\s*/\s*script\s*>)`, // Cross-site scripting (XSS)
685-
`(?i)(\b(?:or|and)\b\s*[\w-]*\s*=\s*[\w-]*\s*\b(?:or|and)\b\s*[^\s]+)`, // SQL injection
686-
`(?i)(['"\s]exec(?:ute)?\s*[(\s]*\s*@\w+\s*)`, // SQL injection (EXEC, EXECUTE)
687-
`(?i)(['"\s]union\s*all\s*select\s*[\w\s,]+(?:from|into|where)\s*\w+)`, // SQL injection (UNION ALL SELECT)
688-
// Private SSH keys
687+
// Additional secret patterns (NOT vulnerability scanners like XSS/SQLi - those were removed to reduce noise)
688+
return []string{
689+
// Private SSH/RSA keys (full block)
689690
`-----BEGIN\sRSA\sPRIVATE\sKEY-----[\s\S]+-----END\sRSA\sPRIVATE\sKEY-----`,
690-
// S3 Bucket URLs
691+
// S3 Bucket URLs (may indicate credential exposure)
691692
`(?i)s3\.amazonaws\.com/[\w\-\.]+`,
692-
// Note: IP address pattern removed - too many false positives in docs/examples
693-
// If needed, filter reserved IPs (127.0.0.1, RFC1918) before reporting
694-
// Basic Authentication credentials
693+
// Basic Authentication credentials in URLs
695694
`(?i)(?:http|https)://\w+:\w+@[\w\-\.]+`,
696695
// JWT tokens
697696
`(?i)ey(?:J[a-zA-Z0-9_-]+)[.](?:[a-zA-Z0-9_-]+)[.](?:[a-zA-Z0-9_-]+)`,
698-
// Connection strings (such as database connections)
697+
// Connection strings (database)
699698
`(?i)(?:Server|Host)=([\w\.-]+);\s*(?:Port|Database|User\s*ID|Password)=([^;\s]+)(?:;\s*(?:Port|Database|User\s*ID|Password)=([^;\s]+))*`,
700-
// Path traversal attempts
701-
// `(\.\./|\.\.\\)`,
702-
// Open redirects
703-
// `(?i)(?:(?:https?|ftp)://|%3A%2F%2F)[^\s&]+(?:\s|%20)*(?:\b(?:and|or)\b\s*[\w-]*\s*=\s*[\w-]*\s*\b(?:and|or)\b\s*[^\s]+)?`,
704-
// UPLOAD MISCONFIG
705-
//`(?i)enctype\s*=\s*['"]multipart/form-data['"]`,
706-
// Headers
707-
//`(?i)<(title|head)>`,
699+
// Encryption keys
708700
`(?i)encryPublicKey\s*=\s*"([^"]*)"`,
709701
`(?i)decryPrivateKey\s*=\s*"([^"]*)"`,
710702
}
711-
return vulnerabilityPatterns
712703
}
713704

714705
func isRegexPatternLine(line string) bool {
715706
// Skip lines that contain regex pattern definitions (backtick-quoted strings with regex)
716707
// Common in source code defining patterns
717-
trimmed := regexp.MustCompile(`^\s+`).ReplaceAllString(line, "")
708+
trimmed := strings.TrimLeft(line, " \t")
718709

719710
// Check if line is a regex pattern definition (starts with backtick or contains backtick with regex chars)
720-
if regexp.MustCompile("^`.*(?:\\(\\?i\\)|\\\\s|\\\\d|\\[|\\]|\\{|\\}|\\||\\^|\\$).*`").MatchString(trimmed) {
721-
return true
722-
}
723-
724-
return false
711+
return regexLinePattern.MatchString(trimmed)
725712
}
726713

727714
func shouldIgnoreDir(path string) bool {
@@ -759,14 +746,26 @@ func shouldIgnoreFile(info os.FileInfo) bool {
759746
return false
760747
}
761748

762-
func matchAnyGlob(path string, patterns []string) bool {
749+
func matchAnyGlob(relPath string, patterns []string) bool {
763750
for _, p := range patterns {
764751
if p == "" {
765752
continue
766753
}
767-
if ok, _ := filepath.Match(p, path); ok {
754+
// Try matching on full relative path
755+
if ok, _ := filepath.Match(p, relPath); ok {
756+
return true
757+
}
758+
// Also try matching on just the basename for simple patterns
759+
if ok, _ := filepath.Match(p, filepath.Base(relPath)); ok {
768760
return true
769761
}
762+
// Support "dir/*" style: check if pattern prefix matches path segments
763+
if strings.HasSuffix(p, "/*") {
764+
prefix := strings.TrimSuffix(p, "/*")
765+
if strings.HasPrefix(relPath, prefix+"/") || relPath == prefix {
766+
return true
767+
}
768+
}
770769
}
771770
return false
772771
}

main_test.go

Lines changed: 82 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"testing"
88
)
99

10+
const testPath = "pkg/service/foo.go"
11+
1012
func TestDetectContext(t *testing.T) {
1113
cases := []struct {
1214
name string
@@ -15,12 +17,12 @@ func TestDetectContext(t *testing.T) {
1517
want string
1618
}{
1719
{"test file detection", "pkg/service/foo_test.go", "secret := \"foo\"", "test_file"},
18-
{"comment line", "pkg/service/foo.go", "// TODO: handle", "comment"},
19-
{"placeholder brace", "pkg/service/foo.go", "token := \"${API_KEY}\"", "placeholder"},
20-
{"placeholder percent", "pkg/service/foo.go", "set %API_KEY%", "placeholder"},
21-
{"placeholder dollar", "pkg/service/foo.go", "token := \"$SECRET_TOKEN\"", "placeholder"},
22-
{"code with percent formatting", "pkg/service/foo.go", "fmt.Printf(\"token=%s\", token)", "code"},
23-
{"pointer code not comment", "pkg/service/foo.go", "value := foo * bar", "code"},
20+
{"comment line", testPath, "// TODO: handle", "comment"},
21+
{"placeholder brace", testPath, "token := \"${API_KEY}\"", "placeholder"},
22+
{"placeholder percent", testPath, "set %API_KEY%", "placeholder"},
23+
{"placeholder dollar", testPath, "token := \"$SECRET_TOKEN\"", "placeholder"},
24+
{"code with percent formatting", testPath, "fmt.Printf(\"token=%s\", token)", "code"},
25+
{"pointer code not comment", testPath, "value := foo * bar", "code"},
2426
{"markdown documentation", "docs/setup.md", "Example TOKEN=foo", "documentation"},
2527
{"readme file", "README.md", "Set API_KEY=foo", "documentation"},
2628
}
@@ -93,3 +95,77 @@ func TestNoFalsePositivesOnSafeFile(t *testing.T) {
9395
t.Fatalf("expected no secrets, got %d", len(secrets))
9496
}
9597
}
98+
99+
func TestMaybeRedactSecret(t *testing.T) {
100+
s := Secret{
101+
FilePath: "src/app.py",
102+
LineNumber: 10,
103+
Line: "api_key = \"sk_live_abc123xyz789\"",
104+
Match: "sk_live_abc123xyz789",
105+
RuleID: "pattern-0",
106+
Confidence: "high",
107+
}
108+
109+
// Without redaction
110+
noRedact := maybeRedactSecret(s, false)
111+
if noRedact.Match != s.Match {
112+
t.Errorf("expected Match to remain %q, got %q", s.Match, noRedact.Match)
113+
}
114+
if noRedact.Line != s.Line {
115+
t.Errorf("expected Line to remain unchanged, got %q", noRedact.Line)
116+
}
117+
118+
// With redaction
119+
redacted := maybeRedactSecret(s, true)
120+
if redacted.Match != "****REDACTED****" {
121+
t.Errorf("expected Match to be redacted, got %q", redacted.Match)
122+
}
123+
if !strings.Contains(redacted.Line, "****REDACTED****") {
124+
t.Errorf("expected Line to contain redacted marker, got %q", redacted.Line)
125+
}
126+
if strings.Contains(redacted.Line, "sk_live_abc123xyz789") {
127+
t.Errorf("Line should not contain the raw secret after redaction")
128+
}
129+
}
130+
131+
func TestConfidenceScore(t *testing.T) {
132+
cases := []struct {
133+
level string
134+
want int
135+
}{
136+
{"critical", 4},
137+
{"high", 3},
138+
{"medium", 2},
139+
{"low", 1},
140+
{"CRITICAL", 4}, // case insensitive
141+
{"unknown", 0},
142+
}
143+
for _, tc := range cases {
144+
got := confidenceScore(tc.level)
145+
if got != tc.want {
146+
t.Errorf("confidenceScore(%q) = %d, want %d", tc.level, got, tc.want)
147+
}
148+
}
149+
}
150+
151+
func TestMatchAnyGlob(t *testing.T) {
152+
cases := []struct {
153+
path string
154+
patterns []string
155+
want bool
156+
}{
157+
{"vendor/github.com/foo", []string{"vendor/*"}, true},
158+
{"vendor", []string{"vendor/*"}, true},
159+
{"src/vendor/lib", []string{"vendor/*"}, false}, // only matches top-level vendor
160+
{"test.go", []string{"*.go"}, true},
161+
{"src/app/test.go", []string{"*.go"}, true}, // matches basename
162+
{"README.md", []string{"*.txt", "*.md"}, true},
163+
{"main.py", []string{"*.go"}, false},
164+
}
165+
for _, tc := range cases {
166+
got := matchAnyGlob(tc.path, tc.patterns)
167+
if got != tc.want {
168+
t.Errorf("matchAnyGlob(%q, %v) = %v, want %v", tc.path, tc.patterns, got, tc.want)
169+
}
170+
}
171+
}

0 commit comments

Comments
 (0)