172 lines
4.5 KiB
Go
172 lines
4.5 KiB
Go
package output
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/csv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/salvacybersec/keyhunter/pkg/engine"
|
|
)
|
|
|
|
func csvSampleFinding() engine.Finding {
|
|
return engine.Finding{
|
|
ProviderName: "anthropic",
|
|
KeyValue: "sk-ant-api03-FULLKEY1234567890",
|
|
KeyMasked: "sk-ant-a...7890",
|
|
Confidence: "medium",
|
|
Source: "src/client.go",
|
|
SourceType: "file",
|
|
LineNumber: 17,
|
|
DetectedAt: time.Date(2026, 4, 5, 8, 15, 0, 0, time.UTC),
|
|
}
|
|
}
|
|
|
|
func TestCSVFormatter_RegisteredUnderCSV(t *testing.T) {
|
|
f, err := Get("csv")
|
|
if err != nil {
|
|
t.Fatalf("Get(\"csv\") error: %v", err)
|
|
}
|
|
if _, ok := f.(CSVFormatter); !ok {
|
|
t.Fatalf("Get(\"csv\") returned %T, want CSVFormatter", f)
|
|
}
|
|
}
|
|
|
|
func TestCSVFormatter_HeaderOnly(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
if err := (CSVFormatter{}).Format(nil, &buf, Options{}); err != nil {
|
|
t.Fatalf("Format error: %v", err)
|
|
}
|
|
r := csv.NewReader(strings.NewReader(buf.String()))
|
|
rows, err := r.ReadAll()
|
|
if err != nil {
|
|
t.Fatalf("csv parse error: %v\n%s", err, buf.String())
|
|
}
|
|
if len(rows) != 1 {
|
|
t.Fatalf("want 1 row (header only), got %d: %v", len(rows), rows)
|
|
}
|
|
wantHeader := []string{"id", "provider", "confidence", "key", "source", "line", "detected_at", "verified", "verify_status"}
|
|
if len(rows[0]) != len(wantHeader) {
|
|
t.Fatalf("header length: got %d want %d", len(rows[0]), len(wantHeader))
|
|
}
|
|
for i, col := range wantHeader {
|
|
if rows[0][i] != col {
|
|
t.Errorf("header[%d]: got %q want %q", i, rows[0][i], col)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCSVFormatter_RowMasked(t *testing.T) {
|
|
f := csvSampleFinding()
|
|
var buf bytes.Buffer
|
|
if err := (CSVFormatter{}).Format([]engine.Finding{f}, &buf, Options{}); err != nil {
|
|
t.Fatalf("Format error: %v", err)
|
|
}
|
|
r := csv.NewReader(strings.NewReader(buf.String()))
|
|
rows, err := r.ReadAll()
|
|
if err != nil {
|
|
t.Fatalf("csv parse error: %v", err)
|
|
}
|
|
if len(rows) != 2 {
|
|
t.Fatalf("want 2 rows, got %d", len(rows))
|
|
}
|
|
row := rows[1]
|
|
if row[0] != "0" {
|
|
t.Errorf("id: got %q want 0", row[0])
|
|
}
|
|
if row[1] != "anthropic" {
|
|
t.Errorf("provider: got %q", row[1])
|
|
}
|
|
if row[2] != "medium" {
|
|
t.Errorf("confidence: got %q", row[2])
|
|
}
|
|
if row[3] != f.KeyMasked {
|
|
t.Errorf("key (masked): got %q want %q", row[3], f.KeyMasked)
|
|
}
|
|
if row[4] != "src/client.go" {
|
|
t.Errorf("source: got %q", row[4])
|
|
}
|
|
if row[5] != "17" {
|
|
t.Errorf("line: got %q", row[5])
|
|
}
|
|
if row[6] != "2026-04-05T08:15:00Z" {
|
|
t.Errorf("detected_at: got %q", row[6])
|
|
}
|
|
if row[7] != "false" {
|
|
t.Errorf("verified: got %q", row[7])
|
|
}
|
|
}
|
|
|
|
func TestCSVFormatter_Unmask(t *testing.T) {
|
|
f := csvSampleFinding()
|
|
var buf bytes.Buffer
|
|
if err := (CSVFormatter{}).Format([]engine.Finding{f}, &buf, Options{Unmask: true}); err != nil {
|
|
t.Fatalf("Format error: %v", err)
|
|
}
|
|
rows, err := csv.NewReader(&buf).ReadAll()
|
|
if err != nil {
|
|
t.Fatalf("csv parse error: %v", err)
|
|
}
|
|
if rows[1][3] != f.KeyValue {
|
|
t.Errorf("key (unmasked): got %q want %q", rows[1][3], f.KeyValue)
|
|
}
|
|
}
|
|
|
|
func TestCSVFormatter_QuotesCommaInSource(t *testing.T) {
|
|
f := csvSampleFinding()
|
|
f.Source = "path, with, commas.txt"
|
|
var buf bytes.Buffer
|
|
if err := (CSVFormatter{}).Format([]engine.Finding{f}, &buf, Options{}); err != nil {
|
|
t.Fatalf("Format error: %v", err)
|
|
}
|
|
// Round-trip must preserve the comma-containing source verbatim.
|
|
rows, err := csv.NewReader(&buf).ReadAll()
|
|
if err != nil {
|
|
t.Fatalf("csv parse error: %v", err)
|
|
}
|
|
if rows[1][4] != "path, with, commas.txt" {
|
|
t.Errorf("source: got %q", rows[1][4])
|
|
}
|
|
}
|
|
|
|
func TestCSVFormatter_VerifiedRow(t *testing.T) {
|
|
f := csvSampleFinding()
|
|
f.Verified = true
|
|
f.VerifyStatus = "live"
|
|
var buf bytes.Buffer
|
|
if err := (CSVFormatter{}).Format([]engine.Finding{f}, &buf, Options{}); err != nil {
|
|
t.Fatalf("Format error: %v", err)
|
|
}
|
|
rows, err := csv.NewReader(&buf).ReadAll()
|
|
if err != nil {
|
|
t.Fatalf("csv parse error: %v", err)
|
|
}
|
|
if rows[1][7] != "true" {
|
|
t.Errorf("verified: got %q want true", rows[1][7])
|
|
}
|
|
if rows[1][8] != "live" {
|
|
t.Errorf("verify_status: got %q want live", rows[1][8])
|
|
}
|
|
}
|
|
|
|
func TestCSVFormatter_MultipleRowsIncrementID(t *testing.T) {
|
|
a := csvSampleFinding()
|
|
b := csvSampleFinding()
|
|
b.ProviderName = "openai"
|
|
var buf bytes.Buffer
|
|
if err := (CSVFormatter{}).Format([]engine.Finding{a, b}, &buf, Options{}); err != nil {
|
|
t.Fatalf("Format error: %v", err)
|
|
}
|
|
rows, err := csv.NewReader(&buf).ReadAll()
|
|
if err != nil {
|
|
t.Fatalf("csv parse error: %v", err)
|
|
}
|
|
if len(rows) != 3 {
|
|
t.Fatalf("want 3 rows, got %d", len(rows))
|
|
}
|
|
if rows[1][0] != "0" || rows[2][0] != "1" {
|
|
t.Errorf("ids: got %q,%q want 0,1", rows[1][0], rows[2][0])
|
|
}
|
|
}
|