From 50e15f90944fd6e1dd0215d7e6f4a82ab0168556 Mon Sep 17 00:00:00 2001 From: StephenJarso Date: Tue, 16 Jun 2026 02:27:17 +0300 Subject: [PATCH 01/23] feat(auth): add provider token detectors --- internal/detectors/auth.go | 56 +++++++++++++++++++++++++++++++++ internal/detectors/auth_test.go | 10 ++++-- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/internal/detectors/auth.go b/internal/detectors/auth.go index 9a79960..6d4356c 100644 --- a/internal/detectors/auth.go +++ b/internal/detectors/auth.go @@ -37,6 +37,12 @@ var ( telegramBotTokenPattern = regexp.MustCompile(`[0-9]{8,10}:[A-Za-z0-9\-_]{35,}`) azureKeyPattern = regexp.MustCompile(`-----BEGIN AZURE KEY VAULT-----`) personalAccessTokenPattern = regexp.MustCompile(`[A-Za-z0-9\-_]{20,}\.[A-Za-z0-9\-_]{60,}`) + // New patterns + figmaTokenPattern = regexp.MustCompile(`figd_[A-Za-z0-9]{60,}`) + notionTokenPattern = regexp.MustCompile(`secret_[A-Za-z0-9]{43}`) + linearTokenPattern = regexp.MustCompile(`lin_api_[A-Za-z0-9]{64}`) + intercomTokenPattern = regexp.MustCompile(`[A-Za-z0-9]{40,}`) + auth0TokenPattern = regexp.MustCompile(`[A-Za-z0-9\-_]{80,}\.[A-Za-z0-9\-_]{32,}`) ) func (d *AuthDetector) Detect(content string, filename string) ([]Finding, error) { @@ -258,6 +264,56 @@ func (d *AuthDetector) Detect(content string, filename string) ([]Finding, error Message: "Personal access token found", }) } + + if figmaTokenPattern.MatchString(line) { + findings = append(findings, Finding{ + Rule: d.Name(), + Severity: High, + File: filename, + Line: lineNum + 1, + Message: "Figma token found", + }) + } + + if notionTokenPattern.MatchString(line) { + findings = append(findings, Finding{ + Rule: d.Name(), + Severity: High, + File: filename, + Line: lineNum + 1, + Message: "Notion token found", + }) + } + + if linearTokenPattern.MatchString(line) { + findings = append(findings, Finding{ + Rule: d.Name(), + Severity: High, + File: filename, + Line: lineNum + 1, + Message: "Linear API token found", + }) + } + + if auth0TokenPattern.MatchString(line) { + findings = append(findings, Finding{ + Rule: d.Name(), + Severity: High, + File: filename, + Line: lineNum + 1, + Message: "Auth0 token found", + }) + } + + if intercomTokenPattern.MatchString(line) { + findings = append(findings, Finding{ + Rule: d.Name(), + Severity: High, + File: filename, + Line: lineNum + 1, + Message: "Intercom token found", + }) + } } return findings, nil diff --git a/internal/detectors/auth_test.go b/internal/detectors/auth_test.go index ac09e6d..1ddbd75 100644 --- a/internal/detectors/auth_test.go +++ b/internal/detectors/auth_test.go @@ -130,7 +130,7 @@ func TestAuthDetector_Detect(t *testing.T) { { name: "slack token", filename: "config.go", - content: "slack_token = 'xoxb-1234567890-123456789012-ABCDEFGHIJKLMNO'", + content: "slack_token = 'xoxb-test0000000-test00000000000-TESTTESTTEST'", wantMin: 1, }, { @@ -160,9 +160,15 @@ func TestAuthDetector_Detect(t *testing.T) { { name: "multiple slack tokens", filename: "config.go", - content: "token1 = 'xoxb-1234567890-123456789012-ABCDEFGHIJKLMNO'\ntoken2 = 'xoxa-1234567890-123456789012-ABCDEFGHIJKLMNO'", + content: "token1 = 'xoxb-test0000000-test00000000000-TESTTESTTEST'\ntoken2 = 'xoxa-test0000000-test00000000000-TESTTESTTEST'", wantMin: 2, }, + { + name: "intercom token", + filename: "config.go", + content: "intercom_token = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'", + wantMin: 1, + }, } for _, tt := range tests { From 36911ea29e037f83255d2353eacb26b55e23d9b6 Mon Sep 17 00:00:00 2001 From: StephenJarso Date: Tue, 16 Jun 2026 02:27:26 +0300 Subject: [PATCH 02/23] test(scanner): add binary detection benchmarks --- internal/scanner/file_test.go | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/internal/scanner/file_test.go b/internal/scanner/file_test.go index 61cb483..7999739 100644 --- a/internal/scanner/file_test.go +++ b/internal/scanner/file_test.go @@ -207,3 +207,38 @@ func TestIsBinaryFileInSubdir(t *testing.T) { t.Error("IsBinaryFile() = true, want false for text file in subdir") } } + +func BenchmarkIsBinary(b *testing.B) { + textData := make([]byte, 512) + for i := range textData { + textData[i] = 'a' + } + + binaryData := make([]byte, 512) + for i := range binaryData { + binaryData[i] = byte(i % 256) + } + binaryData[100] = 0 // Add null byte + + b.Run("text data", func(b *testing.B) { + for i := 0; i < b.N; i++ { + IsBinary(textData) + } + }) + + b.Run("binary data", func(b *testing.B) { + for i := 0; i < b.N; i++ { + IsBinary(binaryData) + } + }) +} + +func BenchmarkBufferPool(b *testing.B) { + b.Run("get and put", func(b *testing.B) { + for i := 0; i < b.N; i++ { + bufPtr := bufferPool.Get().(*[]byte) + _ = (*bufPtr)[0] + bufferPool.Put(bufPtr) + } + }) +} From 3d6153f5e6cba162401e1995e669163f6fa4c681 Mon Sep 17 00:00:00 2001 From: StephenJarso Date: Tue, 16 Jun 2026 02:28:16 +0300 Subject: [PATCH 03/23] test(auth): add provider token coverage --- internal/detectors/auth_test.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/internal/detectors/auth_test.go b/internal/detectors/auth_test.go index 1ddbd75..b412df3 100644 --- a/internal/detectors/auth_test.go +++ b/internal/detectors/auth_test.go @@ -163,6 +163,30 @@ func TestAuthDetector_Detect(t *testing.T) { content: "token1 = 'xoxb-test0000000-test00000000000-TESTTESTTEST'\ntoken2 = 'xoxa-test0000000-test00000000000-TESTTESTTEST'", wantMin: 2, }, + { + name: "figma token", + filename: "config.go", + content: "figma_token = 'figd_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGH'", + wantMin: 1, + }, + { + name: "notion token", + filename: "config.go", + content: "notion_token = 'secret_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrs'", + wantMin: 1, + }, + { + name: "linear api token", + filename: "config.go", + content: "linear_api_token = 'lin_api_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGH'", + wantMin: 1, + }, + { + name: "auth0 token", + filename: "config.go", + content: "auth0_token = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJ.ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijkl'", + wantMin: 1, + }, { name: "intercom token", filename: "config.go", From fa74758850f90222a7aa235f668eff09a74bb0cf Mon Sep 17 00:00:00 2001 From: StephenJarso Date: Tue, 16 Jun 2026 02:28:57 +0300 Subject: [PATCH 04/23] refactor(auth): centralize provider token checks --- internal/detectors/auth.go | 83 +++++++++++++------------------------- 1 file changed, 29 insertions(+), 54 deletions(-) diff --git a/internal/detectors/auth.go b/internal/detectors/auth.go index 6d4356c..561b8f8 100644 --- a/internal/detectors/auth.go +++ b/internal/detectors/auth.go @@ -37,14 +37,27 @@ var ( telegramBotTokenPattern = regexp.MustCompile(`[0-9]{8,10}:[A-Za-z0-9\-_]{35,}`) azureKeyPattern = regexp.MustCompile(`-----BEGIN AZURE KEY VAULT-----`) personalAccessTokenPattern = regexp.MustCompile(`[A-Za-z0-9\-_]{20,}\.[A-Za-z0-9\-_]{60,}`) - // New patterns - figmaTokenPattern = regexp.MustCompile(`figd_[A-Za-z0-9]{60,}`) - notionTokenPattern = regexp.MustCompile(`secret_[A-Za-z0-9]{43}`) - linearTokenPattern = regexp.MustCompile(`lin_api_[A-Za-z0-9]{64}`) - intercomTokenPattern = regexp.MustCompile(`[A-Za-z0-9]{40,}`) - auth0TokenPattern = regexp.MustCompile(`[A-Za-z0-9\-_]{80,}\.[A-Za-z0-9\-_]{32,}`) + figmaTokenPattern = regexp.MustCompile(`figd_[A-Za-z0-9]{60,}`) + notionTokenPattern = regexp.MustCompile(`secret_[A-Za-z0-9]{43}`) + linearTokenPattern = regexp.MustCompile(`lin_api_[A-Za-z0-9]{64}`) + intercomTokenPattern = regexp.MustCompile(`[A-Za-z0-9]{40,}`) + auth0TokenPattern = regexp.MustCompile(`[A-Za-z0-9\-_]{80,}\.[A-Za-z0-9\-_]{32,}`) ) +type authRule struct { + pattern *regexp.Regexp + message string + severity Severity +} + +var providerAuthRules = []authRule{ + {pattern: figmaTokenPattern, message: "Figma token found", severity: High}, + {pattern: notionTokenPattern, message: "Notion token found", severity: High}, + {pattern: linearTokenPattern, message: "Linear API token found", severity: High}, + {pattern: auth0TokenPattern, message: "Auth0 token found", severity: High}, + {pattern: intercomTokenPattern, message: "Intercom token found", severity: High}, +} + func (d *AuthDetector) Detect(content string, filename string) ([]Finding, error) { var findings []Finding lines := strings.Split(content, "\n") @@ -265,54 +278,16 @@ func (d *AuthDetector) Detect(content string, filename string) ([]Finding, error }) } - if figmaTokenPattern.MatchString(line) { - findings = append(findings, Finding{ - Rule: d.Name(), - Severity: High, - File: filename, - Line: lineNum + 1, - Message: "Figma token found", - }) - } - - if notionTokenPattern.MatchString(line) { - findings = append(findings, Finding{ - Rule: d.Name(), - Severity: High, - File: filename, - Line: lineNum + 1, - Message: "Notion token found", - }) - } - - if linearTokenPattern.MatchString(line) { - findings = append(findings, Finding{ - Rule: d.Name(), - Severity: High, - File: filename, - Line: lineNum + 1, - Message: "Linear API token found", - }) - } - - if auth0TokenPattern.MatchString(line) { - findings = append(findings, Finding{ - Rule: d.Name(), - Severity: High, - File: filename, - Line: lineNum + 1, - Message: "Auth0 token found", - }) - } - - if intercomTokenPattern.MatchString(line) { - findings = append(findings, Finding{ - Rule: d.Name(), - Severity: High, - File: filename, - Line: lineNum + 1, - Message: "Intercom token found", - }) + for _, rule := range providerAuthRules { + if rule.pattern.MatchString(line) { + findings = append(findings, Finding{ + Rule: d.Name(), + Severity: rule.severity, + File: filename, + Line: lineNum + 1, + Message: rule.message, + }) + } } } From 8e3214600ba5360add3cbfd391be165375659bf5 Mon Sep 17 00:00:00 2001 From: StephenJarso Date: Tue, 16 Jun 2026 02:29:28 +0300 Subject: [PATCH 05/23] test(auth): assert provider finding metadata --- internal/detectors/auth_test.go | 75 +++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/internal/detectors/auth_test.go b/internal/detectors/auth_test.go index b412df3..fcedb2c 100644 --- a/internal/detectors/auth_test.go +++ b/internal/detectors/auth_test.go @@ -227,3 +227,78 @@ func TestAuthDetector_Detect_NoError(t *testing.T) { t.Fatalf("Detect() error = %v, want nil", err) } } + +func TestAuthDetector_DetectProviderMessages(t *testing.T) { + tests := []struct { + name string + content string + want string + }{ + { + name: "figma", + content: "figma_token = 'figd_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGH'", + want: "Figma token found", + }, + { + name: "notion", + content: "notion_token = 'secret_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrs'", + want: "Notion token found", + }, + { + name: "linear", + content: "linear_api_token = 'lin_api_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGH'", + want: "Linear API token found", + }, + { + name: "auth0", + content: "auth0_token = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJ.ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijkl'", + want: "Auth0 token found", + }, + { + name: "intercom", + content: "intercom_token = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'", + want: "Intercom token found", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &AuthDetector{} + got, err := d.Detect(tt.content, "config.go") + if err != nil { + t.Fatalf("Detect() error = %v", err) + } + if len(got) != 1 { + t.Fatalf("Detect() returned %d findings, want 1", len(got)) + } + if got[0].Message != tt.want { + t.Errorf("Message = %q, want %q", got[0].Message, tt.want) + } + }) + } +} + +func TestAuthDetector_DetectProviderLineNumbers(t *testing.T) { + d := &AuthDetector{} + got, err := d.Detect("line1\nintercom_token = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'", "config.go") + if err != nil { + t.Fatalf("Detect() error = %v", err) + } + if len(got) != 1 { + t.Fatalf("Detect() returned %d findings, want 1", len(got)) + } + if got[0].Line != 2 { + t.Errorf("Line = %d, want 2", got[0].Line) + } +} + +func TestAuthDetector_DetectProviderWhitespaceComments(t *testing.T) { + d := &AuthDetector{} + got, err := d.Detect(" # intercom_token = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'", "config.go") + if err != nil { + t.Fatalf("Detect() error = %v", err) + } + if len(got) != 0 { + t.Fatalf("Detect() returned %d findings, want 0", len(got)) + } +} From 7a1c89aa95bcfba5556958dbe54d23cd17974313 Mon Sep 17 00:00:00 2001 From: StephenJarso Date: Tue, 16 Jun 2026 02:29:42 +0300 Subject: [PATCH 06/23] refactor(auth): narrow intercom token matching --- internal/detectors/auth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/detectors/auth.go b/internal/detectors/auth.go index 561b8f8..0431ab8 100644 --- a/internal/detectors/auth.go +++ b/internal/detectors/auth.go @@ -40,7 +40,7 @@ var ( figmaTokenPattern = regexp.MustCompile(`figd_[A-Za-z0-9]{60,}`) notionTokenPattern = regexp.MustCompile(`secret_[A-Za-z0-9]{43}`) linearTokenPattern = regexp.MustCompile(`lin_api_[A-Za-z0-9]{64}`) - intercomTokenPattern = regexp.MustCompile(`[A-Za-z0-9]{40,}`) + intercomTokenPattern = regexp.MustCompile(`(?i)(intercom(?:_|[-[:space:]])?(?:access[_-]?)?token|access[_-]?token)\s*[:=]\s*['"]?[A-Za-z0-9]{40,}['"]?`) auth0TokenPattern = regexp.MustCompile(`[A-Za-z0-9\-_]{80,}\.[A-Za-z0-9\-_]{32,}`) ) From 5d61e31613b103d4d925a0d8d4a40de2b0863c26 Mon Sep 17 00:00:00 2001 From: StephenJarso Date: Tue, 16 Jun 2026 02:29:55 +0300 Subject: [PATCH 07/23] test(auth): add intercom false-positive guard --- internal/detectors/auth_test.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/internal/detectors/auth_test.go b/internal/detectors/auth_test.go index fcedb2c..8c3fc6d 100644 --- a/internal/detectors/auth_test.go +++ b/internal/detectors/auth_test.go @@ -302,3 +302,14 @@ func TestAuthDetector_DetectProviderWhitespaceComments(t *testing.T) { t.Fatalf("Detect() returned %d findings, want 0", len(got)) } } + +func TestAuthDetector_DetectIntercomTokenFalsePositiveGuard(t *testing.T) { + d := &AuthDetector{} + got, err := d.Detect("random_string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'", "config.go") + if err != nil { + t.Fatalf("Detect() error = %v", err) + } + if len(got) != 0 { + t.Fatalf("Detect() returned %d findings, want 0", len(got)) + } +} From 5a125d186975a99bea95ece3db0eb52d3a14dcd2 Mon Sep 17 00:00:00 2001 From: StephenJarso Date: Tue, 16 Jun 2026 02:30:17 +0300 Subject: [PATCH 08/23] refactor(auth): deduplicate provider findings per line --- internal/detectors/auth.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/detectors/auth.go b/internal/detectors/auth.go index 0431ab8..03aa5a3 100644 --- a/internal/detectors/auth.go +++ b/internal/detectors/auth.go @@ -278,8 +278,12 @@ func (d *AuthDetector) Detect(content string, filename string) ([]Finding, error }) } + seenProviderFindings := make(map[string]struct{}, len(providerAuthRules)) for _, rule := range providerAuthRules { if rule.pattern.MatchString(line) { + if _, seen := seenProviderFindings[rule.message]; seen { + continue + } findings = append(findings, Finding{ Rule: d.Name(), Severity: rule.severity, @@ -287,6 +291,7 @@ func (d *AuthDetector) Detect(content string, filename string) ([]Finding, error Line: lineNum + 1, Message: rule.message, }) + seenProviderFindings[rule.message] = struct{}{} } } } From c9c766d02ea3616c8bc71028ebd3da67bcf901fd Mon Sep 17 00:00:00 2001 From: StephenJarso Date: Tue, 16 Jun 2026 02:30:31 +0300 Subject: [PATCH 09/23] test(auth): cover provider findings per line --- internal/detectors/auth_test.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/internal/detectors/auth_test.go b/internal/detectors/auth_test.go index 8c3fc6d..69f5126 100644 --- a/internal/detectors/auth_test.go +++ b/internal/detectors/auth_test.go @@ -313,3 +313,17 @@ func TestAuthDetector_DetectIntercomTokenFalsePositiveGuard(t *testing.T) { t.Fatalf("Detect() returned %d findings, want 0", len(got)) } } + +func TestAuthDetector_DetectProviderOneFindingPerLine(t *testing.T) { + d := &AuthDetector{} + got, err := d.Detect("intercom_token = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'\nintercom_access_token = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'", "config.go") + if err != nil { + t.Fatalf("Detect() error = %v", err) + } + if len(got) != 2 { + t.Fatalf("Detect() returned %d findings, want 2", len(got)) + } + if got[0].Line != 1 || got[1].Line != 2 { + t.Errorf("Lines = [%d, %d], want [1, 2]", got[0].Line, got[1].Line) + } +} From be3559f14b6cbeca085e89a004e64b0aeaaa5c40 Mon Sep 17 00:00:00 2001 From: StephenJarso Date: Tue, 16 Jun 2026 02:30:52 +0300 Subject: [PATCH 10/23] docs(detectors): document provider token auth rules --- docs/detectors.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/detectors.md b/docs/detectors.md index 031aac8..9f6852d 100644 --- a/docs/detectors.md +++ b/docs/detectors.md @@ -38,6 +38,11 @@ The `AUTH_CREDENTIALS` detector identifies various authentication tokens and cre - **Telegram Bot Tokens**: `1234567890:...` - **Azure Key Vault**: `-----BEGIN AZURE KEY VAULT-----` - **Personal Access Tokens**: Various formats +- **Figma Tokens**: `figd_...` +- **Notion Tokens**: `secret_...` +- **Linear API Tokens**: `lin_api_...` +- **Auth0 Tokens**: JWT-like tokens with long segments +- **Intercom Tokens**: 40-character tokens assigned to Intercom access token variables - **SSH/PGP Keys**: Private key headers - **JWT Tokens**: JSON Web Tokens - **Bearer Tokens**: Authorization headers From f2228332350a49aaaabf6698a4d297276daa5bc7 Mon Sep 17 00:00:00 2001 From: StephenJarso Date: Tue, 16 Jun 2026 02:31:12 +0300 Subject: [PATCH 11/23] docs: update supported detector table --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index cb5ed3a..eb70db0 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,7 @@ allowlist: |---------------------------|-----------|---------------------| | AWS Keys | CRITICAL | Low | | GitHub Tokens | CRITICAL | Low | +| Provider Access Tokens | HIGH | Low | | `.env` Files | CRITICAL | None | | Generic API Keys | HIGH | Medium | | Private SSH Keys | CRITICAL | Low | From cee93b412ffdf1a6d78bd52d7c311294115e3109 Mon Sep 17 00:00:00 2001 From: StephenJarso Date: Tue, 16 Jun 2026 02:31:25 +0300 Subject: [PATCH 12/23] docs: update changelog for provider token detectors --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 101a889..442c152 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Telegram bot token detector - Azure Key Vault detector - Personal access token detector +- Provider access token detectors for Figma, Notion, Linear, Auth0, and Intercom +- Scanner binary detection benchmarks - Hardcoded password pattern detection - Connection string pattern detection - API key header pattern detection From 50d3cdc2a551b487e14df1484d9fda5f52ea2acf Mon Sep 17 00:00:00 2001 From: StephenJarso Date: Tue, 16 Jun 2026 02:31:55 +0300 Subject: [PATCH 13/23] refactor(scanner): reuse file info helper in ScanFile --- internal/scanner/scanner.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/scanner/scanner.go b/internal/scanner/scanner.go index f354439..a11c475 100644 --- a/internal/scanner/scanner.go +++ b/internal/scanner/scanner.go @@ -144,7 +144,7 @@ func (s *Scanner) ScanFile(path string) ([]detectors.Finding, error) { return nil, fmt.Errorf("file is ignored: %s", path) } - fileInfo, err := os.Stat(path) + fileInfo, err := GetFileInfo(path) if err != nil { return nil, err } From 9a79625c90cbbe7e5246a8c397ad4a7ab1418b16 Mon Sep 17 00:00:00 2001 From: StephenJarso Date: Tue, 16 Jun 2026 02:32:14 +0300 Subject: [PATCH 14/23] test(scanner): add auth detector integration coverage --- internal/scanner/auth_integration_test.go | 76 +++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 internal/scanner/auth_integration_test.go diff --git a/internal/scanner/auth_integration_test.go b/internal/scanner/auth_integration_test.go new file mode 100644 index 0000000..5b14b13 --- /dev/null +++ b/internal/scanner/auth_integration_test.go @@ -0,0 +1,76 @@ +package scanner + +import ( + "os" + "path/filepath" + "testing" + + "secure-push/internal/config" + "secure-push/internal/detectors" + "secure-push/internal/logger" +) + +func TestIntegrationScanWithAuthDetector(t *testing.T) { + tmpDir := t.TempDir() + configFile := filepath.Join(tmpDir, "config.go") + err := os.WriteFile(configFile, []byte("intercom_token = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'\n"), 0o644) + if err != nil { + t.Fatal(err) + } + + scanner := New([]detectors.Detector{&detectors.AuthDetector{}}, config.DefaultConfig(), logger.New(logger.Debug)) + findings, err := scanner.Scan(tmpDir) + if err != nil { + t.Fatalf("Scan() error = %v", err) + } + if len(findings) != 1 { + t.Fatalf("Scan() returned %d findings, want 1", len(findings)) + } + if findings[0].Message != "Intercom token found" { + t.Errorf("Message = %q, want Intercom token found", findings[0].Message) + } +} + +func TestIntegrationScanFileWithAuthDetector(t *testing.T) { + tmpFile, err := os.CreateTemp(t.TempDir(), "config-*.go") + if err != nil { + t.Fatal(err) + } + if _, err := tmpFile.WriteString("intercom_token = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'\n"); err != nil { + t.Fatal(err) + } + if err := tmpFile.Close(); err != nil { + t.Fatal(err) + } + + scanner := New([]detectors.Detector{&detectors.AuthDetector{}}, config.DefaultConfig(), logger.New(logger.Debug)) + findings, err := scanner.ScanFile(tmpFile.Name()) + if err != nil { + t.Fatalf("ScanFile() error = %v", err) + } + if len(findings) != 1 { + t.Fatalf("ScanFile() returned %d findings, want 1", len(findings)) + } + if findings[0].Line != 1 { + t.Errorf("Line = %d, want 1", findings[0].Line) + } +} + +func TestIntegrationScanFileSkipsBinaryFile(t *testing.T) { + tmpFile, err := os.CreateTemp(t.TempDir(), "binary-*.bin") + if err != nil { + t.Fatal(err) + } + if _, err := tmpFile.Write([]byte{0x00, 0x01, 0x02}); err != nil { + t.Fatal(err) + } + if err := tmpFile.Close(); err != nil { + t.Fatal(err) + } + + scanner := New([]detectors.Detector{&detectors.AuthDetector{}}, config.DefaultConfig(), logger.New(logger.Debug)) + _, err = scanner.ScanFile(tmpFile.Name()) + if err == nil { + t.Fatal("ScanFile() error = nil, want binary file error") + } +} From bffe2d7b4e3c6ff235a7824d177d77a0e18bdd00 Mon Sep 17 00:00:00 2001 From: StephenJarso Date: Tue, 16 Jun 2026 02:32:38 +0300 Subject: [PATCH 15/23] test(scanner): cover hidden directory walk behavior --- internal/scanner/walk_test.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/internal/scanner/walk_test.go b/internal/scanner/walk_test.go index 968fa74..000122b 100644 --- a/internal/scanner/walk_test.go +++ b/internal/scanner/walk_test.go @@ -35,6 +35,30 @@ func TestWalkDir(t *testing.T) { } } +func TestWalkDirSkipsHiddenDirectory(t *testing.T) { + tmpDir := t.TempDir() + hiddenDir := filepath.Join(tmpDir, ".hidden") + if err := os.MkdirAll(hiddenDir, 0o755); err != nil { + t.Fatal(err) + } + hiddenFile := filepath.Join(hiddenDir, "secret.txt") + if err := os.WriteFile(hiddenFile, []byte("secret"), 0o644); err != nil { + t.Fatal(err) + } + + var visited []string + err := WalkDir(tmpDir, func(path string, info os.FileInfo) error { + visited = append(visited, filepath.Base(path)) + return nil + }) + if err != nil { + t.Fatalf("WalkDir() error = %v", err) + } + if len(visited) != 0 { + t.Errorf("WalkDir() visited %d files, want 0", len(visited)) + } +} + func TestGetFileSize(t *testing.T) { tmpFile, err := os.CreateTemp("", "test-*.txt") if err != nil { From d7b613f591ee957a178b65833a3c119dad0c1018 Mon Sep 17 00:00:00 2001 From: StephenJarso Date: Tue, 16 Jun 2026 02:32:50 +0300 Subject: [PATCH 16/23] test(scanner): cover relative path error handling --- internal/scanner/walk_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/internal/scanner/walk_test.go b/internal/scanner/walk_test.go index 000122b..2534a5b 100644 --- a/internal/scanner/walk_test.go +++ b/internal/scanner/walk_test.go @@ -132,6 +132,13 @@ func TestGetRelativePath(t *testing.T) { } } +func TestGetRelativePathError(t *testing.T) { + _, err := GetRelativePath("/tmp/secure-push-a", "/tmp/secure-push-b") + if err == nil { + t.Fatal("GetRelativePath() error = nil, want error") + } +} + func TestValidatePath(t *testing.T) { t.Run("empty path", func(t *testing.T) { err := ValidatePath("") From 55f211a3564c364be42dbaed990dd71ed8d415b3 Mon Sep 17 00:00:00 2001 From: StephenJarso Date: Tue, 16 Jun 2026 02:33:17 +0300 Subject: [PATCH 17/23] test(scanner): cover missing path directory checks --- internal/scanner/walk_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/scanner/walk_test.go b/internal/scanner/walk_test.go index 2534a5b..8c79aa7 100644 --- a/internal/scanner/walk_test.go +++ b/internal/scanner/walk_test.go @@ -118,6 +118,12 @@ func TestIsDir(t *testing.T) { } } +func TestIsDirWithMissingPath(t *testing.T) { + if IsDir("/nonexistent/path") { + t.Error("IsDir() = true, want false for missing path") + } +} + func TestGetRelativePath(t *testing.T) { base := "/home/user/project" path := "/home/user/project/src/main.go" From fea0cae7bc991c1858988879918a419df5b094de Mon Sep 17 00:00:00 2001 From: StephenJarso Date: Tue, 16 Jun 2026 02:33:46 +0300 Subject: [PATCH 18/23] refactor(scanner): use io.EOF for binary read handling --- internal/scanner/file.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/scanner/file.go b/internal/scanner/file.go index 6406102..9de9662 100644 --- a/internal/scanner/file.go +++ b/internal/scanner/file.go @@ -1,6 +1,8 @@ package scanner import ( + "errors" + "io" "os" "path/filepath" "sync" @@ -26,7 +28,7 @@ func IsBinaryFile(path string) (bool, error) { buffer := *bufPtr n, err := file.Read(buffer) - if err != nil && err.Error() != "EOF" { + if err != nil && !errors.Is(err, io.EOF) { return false, err } From d13f29784755fea0aced0552b58cfcdd75b71d47 Mon Sep 17 00:00:00 2001 From: StephenJarso Date: Tue, 16 Jun 2026 02:34:25 +0300 Subject: [PATCH 19/23] test(scanner): cover IsTextFile binary behavior --- internal/scanner/file_test.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/internal/scanner/file_test.go b/internal/scanner/file_test.go index 7999739..a6bc0e5 100644 --- a/internal/scanner/file_test.go +++ b/internal/scanner/file_test.go @@ -128,6 +128,27 @@ func TestIsTextFile(t *testing.T) { }) } +func TestIsTextFileWithBinaryFile(t *testing.T) { + tmpFile, err := os.CreateTemp("", "binary-*.bin") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmpFile.Name()) + + if _, err := tmpFile.Write([]byte{0x00, 0x01, 0x02}); err != nil { + t.Fatal(err) + } + tmpFile.Close() + + isText, err := IsTextFile(tmpFile.Name()) + if err != nil { + t.Fatalf("IsTextFile() error = %v", err) + } + if isText { + t.Error("IsTextFile() = true, want false for binary file") + } +} + func TestIsBinaryFileWithSmallFile(t *testing.T) { // Test that files smaller than 512 bytes are handled correctly tmpFile, err := os.CreateTemp("", "small-*.txt") From ee948228ba5977c7167f315d8e7c8bc76b7af75d Mon Sep 17 00:00:00 2001 From: StephenJarso Date: Tue, 16 Jun 2026 02:35:25 +0300 Subject: [PATCH 20/23] test(scanner): add text classification benchmark --- internal/scanner/file_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/internal/scanner/file_test.go b/internal/scanner/file_test.go index a6bc0e5..b2cea1d 100644 --- a/internal/scanner/file_test.go +++ b/internal/scanner/file_test.go @@ -263,3 +263,15 @@ func BenchmarkBufferPool(b *testing.B) { } }) } + +func BenchmarkTextClassification(b *testing.B) { + textData := make([]byte, 512) + for i := range textData { + textData[i] = 'a' + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = IsBinary(textData) + } +} From 4a35fde2022af66a996b17bf74a3849cef66f39b Mon Sep 17 00:00:00 2001 From: StephenJarso Date: Tue, 16 Jun 2026 02:35:45 +0300 Subject: [PATCH 21/23] docs: add detector test guidance --- docs/development.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/development.md b/docs/development.md index 5f92601..9dca5ae 100644 --- a/docs/development.md +++ b/docs/development.md @@ -56,6 +56,8 @@ make lint 4. Register the detector in `internal/scanner/scanner.go` 5. Update README.md with the new detector +Detector tests should include positive matches, negative matches, line number assertions, and false-positive guards for broad patterns. + ## Adding a New Reporter 1. Create a new file in `internal/reporters/` From 714a199503d8d239705f9927aa3706b21afe2cf8 Mon Sep 17 00:00:00 2001 From: StephenJarso Date: Tue, 16 Jun 2026 02:36:11 +0300 Subject: [PATCH 22/23] test(scanner): cover ignored ScanFile paths --- internal/scanner/auth_integration_test.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/internal/scanner/auth_integration_test.go b/internal/scanner/auth_integration_test.go index 5b14b13..bdefa50 100644 --- a/internal/scanner/auth_integration_test.go +++ b/internal/scanner/auth_integration_test.go @@ -74,3 +74,25 @@ func TestIntegrationScanFileSkipsBinaryFile(t *testing.T) { t.Fatal("ScanFile() error = nil, want binary file error") } } + +func TestIntegrationScanFileRejectsIgnoredFile(t *testing.T) { + tmpFile, err := os.CreateTemp(t.TempDir(), "config-*.go") + if err != nil { + t.Fatal(err) + } + if _, err := tmpFile.WriteString("intercom_token = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'\n"); err != nil { + t.Fatal(err) + } + if err := tmpFile.Close(); err != nil { + t.Fatal(err) + } + + cfg := &config.Config{ + IgnorePaths: []string{tmpFile.Name()}, + } + scanner := New([]detectors.Detector{&detectors.AuthDetector{}}, cfg, logger.New(logger.Debug)) + _, err = scanner.ScanFile(tmpFile.Name()) + if err == nil { + t.Fatal("ScanFile() error = nil, want ignored file error") + } +} From e387a1cbee1dffe89e5fac747620cecd689a1952 Mon Sep 17 00:00:00 2001 From: StephenJarso Date: Tue, 16 Jun 2026 02:36:29 +0300 Subject: [PATCH 23/23] test(scanner): cover disabled auth detector --- internal/scanner/auth_integration_test.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/internal/scanner/auth_integration_test.go b/internal/scanner/auth_integration_test.go index bdefa50..f69660a 100644 --- a/internal/scanner/auth_integration_test.go +++ b/internal/scanner/auth_integration_test.go @@ -96,3 +96,24 @@ func TestIntegrationScanFileRejectsIgnoredFile(t *testing.T) { t.Fatal("ScanFile() error = nil, want ignored file error") } } + +func TestIntegrationScanWithDisabledAuthDetector(t *testing.T) { + tmpDir := t.TempDir() + configFile := filepath.Join(tmpDir, "config.go") + err := os.WriteFile(configFile, []byte("intercom_token = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'\n"), 0o644) + if err != nil { + t.Fatal(err) + } + + cfg := &config.Config{ + DisableDetectors: []string{"AUTH_CREDENTIALS"}, + } + scanner := New([]detectors.Detector{&detectors.AuthDetector{}}, cfg, logger.New(logger.Debug)) + findings, err := scanner.Scan(tmpDir) + if err != nil { + t.Fatalf("Scan() error = %v", err) + } + if len(findings) != 0 { + t.Fatalf("Scan() returned %d findings, want 0", len(findings)) + } +}