feat(07-04): wire keyhunter import command with dedup and DB persist
- Replace import stub with cmd/import.go dispatching to pkg/importer (trufflehog, gitleaks, gitleaks-csv) via --format flag - Reuse openDBWithKey helper so encryption + path resolution match scan/keys - engineToStorage converts engine.Finding -> storage.Finding (Source -> SourcePath) - Add pkg/storage.FindingExistsByKey for idempotent cross-import dedup keyed on (provider, masked key, source path, line number) - cmd/import_test.go: selector table, field conversion, end-to-end trufflehog import with re-run duplicate assertion, unknown-format + missing-file errors - pkg/storage queries_test: FindingExistsByKey hit and four miss cases Delivers IMP-01/02/03 end-to-end.
This commit is contained in:
156
cmd/import_test.go
Normal file
156
cmd/import_test.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/salvacybersec/keyhunter/pkg/engine"
|
||||
"github.com/salvacybersec/keyhunter/pkg/importer"
|
||||
"github.com/salvacybersec/keyhunter/pkg/storage"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSelectImporter(t *testing.T) {
|
||||
cases := []struct {
|
||||
format string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{"trufflehog", "trufflehog", false},
|
||||
{"gitleaks", "gitleaks", false},
|
||||
{"gitleaks-csv", "gitleaks-csv", false},
|
||||
{"bogus", "", true},
|
||||
{"", "", true},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.format, func(t *testing.T) {
|
||||
imp, err := selectImporter(tc.format)
|
||||
if tc.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.want, imp.Name())
|
||||
// Check concrete type matches expectation via interface dispatch.
|
||||
switch tc.format {
|
||||
case "trufflehog":
|
||||
_, ok := imp.(importer.TruffleHogImporter)
|
||||
assert.True(t, ok, "expected TruffleHogImporter")
|
||||
case "gitleaks":
|
||||
_, ok := imp.(importer.GitleaksImporter)
|
||||
assert.True(t, ok, "expected GitleaksImporter")
|
||||
case "gitleaks-csv":
|
||||
_, ok := imp.(importer.GitleaksCSVImporter)
|
||||
assert.True(t, ok, "expected GitleaksCSVImporter")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEngineToStorage(t *testing.T) {
|
||||
ef := engine.Finding{
|
||||
ProviderName: "openai",
|
||||
KeyValue: "sk-abcdefghijklmnop",
|
||||
KeyMasked: "sk-abcde...mnop",
|
||||
Confidence: "high",
|
||||
Source: "a.yml",
|
||||
SourceType: "import:trufflehog",
|
||||
LineNumber: 5,
|
||||
Verified: true,
|
||||
VerifyStatus: "live",
|
||||
VerifyHTTPCode: 200,
|
||||
VerifyMetadata: map[string]string{"org": "acme"},
|
||||
}
|
||||
sf := engineToStorage(ef)
|
||||
assert.Equal(t, "openai", sf.ProviderName)
|
||||
assert.Equal(t, "sk-abcdefghijklmnop", sf.KeyValue)
|
||||
assert.Equal(t, "sk-abcde...mnop", sf.KeyMasked)
|
||||
assert.Equal(t, "high", sf.Confidence)
|
||||
assert.Equal(t, "a.yml", sf.SourcePath, "engine.Source -> storage.SourcePath")
|
||||
assert.Equal(t, "import:trufflehog", sf.SourceType)
|
||||
assert.Equal(t, 5, sf.LineNumber)
|
||||
assert.True(t, sf.Verified)
|
||||
assert.Equal(t, "live", sf.VerifyStatus)
|
||||
assert.Equal(t, 200, sf.VerifyHTTPCode)
|
||||
assert.Equal(t, map[string]string{"org": "acme"}, sf.VerifyMetadata)
|
||||
}
|
||||
|
||||
// seedImportDB spins up a temp SQLite database wired through viper +
|
||||
// KEYHUNTER_PASSPHRASE so runImport -> openDBWithKey resolves to it.
|
||||
func seedImportDB(t *testing.T) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
dbPath := filepath.Join(dir, "import.db")
|
||||
|
||||
viper.Reset()
|
||||
viper.Set("database.path", dbPath)
|
||||
t.Setenv("KEYHUNTER_PASSPHRASE", "test-pass")
|
||||
|
||||
t.Cleanup(func() {
|
||||
viper.Reset()
|
||||
importFormat = ""
|
||||
})
|
||||
return dbPath
|
||||
}
|
||||
|
||||
func TestRunImport_TruffleHogEndToEnd(t *testing.T) {
|
||||
dbPath := seedImportDB(t)
|
||||
|
||||
// Use the canonical testdata shipped by pkg/importer.
|
||||
importFormat = "trufflehog"
|
||||
samplePath, err := filepath.Abs(filepath.Join("..", "pkg", "importer", "testdata", "trufflehog-sample.json"))
|
||||
require.NoError(t, err)
|
||||
_, err = os.Stat(samplePath)
|
||||
require.NoError(t, err, "testdata trufflehog-sample.json must exist")
|
||||
|
||||
var out bytes.Buffer
|
||||
importCmd.SetOut(&out)
|
||||
importCmd.SetErr(&out)
|
||||
|
||||
// First import: all findings are new.
|
||||
err = runImport(importCmd, []string{samplePath})
|
||||
require.NoError(t, err)
|
||||
first := out.String()
|
||||
assert.Contains(t, first, "Imported 3 findings")
|
||||
assert.Contains(t, first, "3 new")
|
||||
assert.Contains(t, first, "0 duplicates")
|
||||
|
||||
// Confirm findings landed in the database.
|
||||
db, err := storage.Open(dbPath)
|
||||
require.NoError(t, err)
|
||||
encKey, err := loadOrCreateEncKey(db, "test-pass")
|
||||
require.NoError(t, err)
|
||||
stored, err := db.ListFindings(encKey)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(stored), 3)
|
||||
require.NoError(t, db.Close())
|
||||
|
||||
// Second import of the same file: everything should now be a duplicate.
|
||||
out.Reset()
|
||||
err = runImport(importCmd, []string{samplePath})
|
||||
require.NoError(t, err)
|
||||
second := out.String()
|
||||
assert.Contains(t, second, "Imported 3 findings")
|
||||
assert.Contains(t, second, "0 new")
|
||||
assert.Contains(t, second, "3 duplicates")
|
||||
}
|
||||
|
||||
func TestRunImport_UnknownFormat(t *testing.T) {
|
||||
_ = seedImportDB(t)
|
||||
importFormat = "bogus"
|
||||
err := runImport(importCmd, []string{"/nonexistent"})
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "unknown format")
|
||||
}
|
||||
|
||||
func TestRunImport_MissingFile(t *testing.T) {
|
||||
_ = seedImportDB(t)
|
||||
importFormat = "trufflehog"
|
||||
err := runImport(importCmd, []string{filepath.Join(t.TempDir(), "does-not-exist.json")})
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "opening")
|
||||
}
|
||||
Reference in New Issue
Block a user