Files
keyhunter/pkg/importer/gitleaks_test.go
salvacybersec 83640ac200 feat(07-02): add Gitleaks JSON + CSV importers
- GitleaksImporter parses native JSON array output to []engine.Finding
- GitleaksCSVImporter parses CSV with header-based column resolution
- normalizeGitleaksRuleID strips suffixes (-api-key, -access-token, ...)
- Shared buildGitleaksFinding helper keeps JSON/CSV paths in lockstep
- Test fixtures + 8 tests covering happy path, empty, invalid, symlink fallback
2026-04-05 23:55:36 +03:00

160 lines
5.1 KiB
Go

package importer
import (
"bytes"
"os"
"strings"
"testing"
)
func loadFixture(t *testing.T, name string) []byte {
t.Helper()
data, err := os.ReadFile("testdata/" + name)
if err != nil {
t.Fatalf("read fixture %s: %v", name, err)
}
return data
}
func TestGitleaksImporter_Name(t *testing.T) {
if (GitleaksImporter{}).Name() != "gitleaks" {
t.Errorf("GitleaksImporter.Name() = %q, want %q", (GitleaksImporter{}).Name(), "gitleaks")
}
if (GitleaksCSVImporter{}).Name() != "gitleaks-csv" {
t.Errorf("GitleaksCSVImporter.Name() = %q, want %q", (GitleaksCSVImporter{}).Name(), "gitleaks-csv")
}
}
func TestGitleaksImporter_JSON(t *testing.T) {
data := loadFixture(t, "gitleaks-sample.json")
findings, err := (GitleaksImporter{}).Import(bytes.NewReader(data))
if err != nil {
t.Fatalf("Import: %v", err)
}
if len(findings) != 3 {
t.Fatalf("expected 3 findings, got %d", len(findings))
}
if findings[0].ProviderName != "openai" {
t.Errorf("findings[0].ProviderName = %q, want %q", findings[0].ProviderName, "openai")
}
if findings[0].KeyValue != "sk-proj-1234567890abcdef1234" {
t.Errorf("findings[0].KeyValue mismatch: %q", findings[0].KeyValue)
}
if findings[0].Source != "config/app.yml" {
t.Errorf("findings[0].Source = %q", findings[0].Source)
}
if findings[0].LineNumber != 12 {
t.Errorf("findings[0].LineNumber = %d, want 12", findings[0].LineNumber)
}
if findings[0].SourceType != "import:gitleaks" {
t.Errorf("findings[0].SourceType = %q", findings[0].SourceType)
}
if findings[0].Confidence != "medium" {
t.Errorf("findings[0].Confidence = %q, want medium", findings[0].Confidence)
}
if findings[0].VerifyStatus != "unverified" {
t.Errorf("findings[0].VerifyStatus = %q, want unverified", findings[0].VerifyStatus)
}
if findings[0].Verified {
t.Errorf("findings[0].Verified should be false")
}
if findings[0].KeyMasked == "" {
t.Errorf("findings[0].KeyMasked should be set")
}
if findings[1].ProviderName != "aws" {
t.Errorf("findings[1].ProviderName = %q, want aws", findings[1].ProviderName)
}
if findings[1].LineNumber != 55 {
t.Errorf("findings[1].LineNumber = %d, want 55", findings[1].LineNumber)
}
if findings[2].ProviderName != "generic" {
t.Errorf("findings[2].ProviderName = %q, want generic", findings[2].ProviderName)
}
}
func TestGitleaksImporter_CSV(t *testing.T) {
data := loadFixture(t, "gitleaks-sample.csv")
findings, err := (GitleaksCSVImporter{}).Import(bytes.NewReader(data))
if err != nil {
t.Fatalf("Import: %v", err)
}
if len(findings) != 3 {
t.Fatalf("expected 3 findings, got %d", len(findings))
}
if findings[0].ProviderName != "openai" {
t.Errorf("findings[0].ProviderName = %q, want openai", findings[0].ProviderName)
}
if findings[0].KeyValue != "sk-proj-1234567890abcdef1234" {
t.Errorf("findings[0].KeyValue = %q", findings[0].KeyValue)
}
if findings[0].Source != "config/app.yml" {
t.Errorf("findings[0].Source = %q", findings[0].Source)
}
if findings[0].LineNumber != 12 {
t.Errorf("findings[0].LineNumber = %d, want 12", findings[0].LineNumber)
}
if findings[1].ProviderName != "aws" {
t.Errorf("findings[1].ProviderName = %q, want aws", findings[1].ProviderName)
}
if findings[2].ProviderName != "generic" {
t.Errorf("findings[2].ProviderName = %q, want generic", findings[2].ProviderName)
}
}
func TestGitleaksImporter_NormalizeRuleID(t *testing.T) {
cases := []struct{ in, out string }{
{"openai-api-key", "openai"},
{"aws-access-token", "aws"},
{"anthropic-api-key", "anthropic"},
{"generic-api-key", "generic"},
{"github-pat", "github-pat"},
{"Some-Secret", "some"},
{"AWS-Access-Token", "aws"},
}
for _, c := range cases {
got := normalizeGitleaksRuleID(c.in)
if got != c.out {
t.Errorf("normalizeGitleaksRuleID(%q) = %q, want %q", c.in, got, c.out)
}
}
}
func TestGitleaksImporter_EmptyArray(t *testing.T) {
findings, err := (GitleaksImporter{}).Import(strings.NewReader("[]"))
if err != nil {
t.Fatalf("Import: %v", err)
}
if len(findings) != 0 {
t.Errorf("expected 0 findings, got %d", len(findings))
}
}
func TestGitleaksImporter_EmptyCSV(t *testing.T) {
header := "RuleID,Commit,File,SymlinkFile,Secret,Match,StartLine,EndLine,StartColumn,EndColumn,Author,Message,Date,Email,Fingerprint,Tags\n"
findings, err := (GitleaksCSVImporter{}).Import(strings.NewReader(header))
if err != nil {
t.Fatalf("Import: %v", err)
}
if len(findings) != 0 {
t.Errorf("expected 0 findings, got %d", len(findings))
}
}
func TestGitleaksImporter_InvalidJSON(t *testing.T) {
_, err := (GitleaksImporter{}).Import(strings.NewReader("{not json"))
if err == nil {
t.Errorf("expected error for invalid JSON")
}
}
func TestGitleaksImporter_SymlinkFallback(t *testing.T) {
jsonInput := `[{"RuleID":"openai-api-key","Secret":"sk-proj-1234567890abcdef1234","File":"","SymlinkFile":"link/config.yml","StartLine":1}]`
findings, err := (GitleaksImporter{}).Import(strings.NewReader(jsonInput))
if err != nil {
t.Fatalf("Import: %v", err)
}
if len(findings) != 1 || findings[0].Source != "link/config.yml" {
t.Errorf("expected symlink fallback source, got %+v", findings)
}
}