Files
keyhunter/pkg/output/csv.go
salvacybersec 03249fb3d1 feat(06-02): implement CSVFormatter with Unmask support
- Fixed 9-column header: id,provider,confidence,key,source,line,detected_at,verified,verify_status
- Uses encoding/csv for automatic quoting of commas/quotes in source paths
- Honors Options.Unmask for key column
- Registers under "csv" in output registry
2026-04-05 23:32:07 +03:00

61 lines
1.6 KiB
Go

package output
import (
"encoding/csv"
"io"
"strconv"
"time"
"github.com/salvacybersec/keyhunter/pkg/engine"
)
func init() {
Register("csv", CSVFormatter{})
}
// CSVFormatter renders findings as comma-separated values with a fixed
// header row. The id column is the zero-based index of the finding within
// the slice -- the storage-layer rowid is intentionally not used here so
// the formatter can run against in-memory results from a scan that has
// not been persisted yet. Standard encoding/csv quoting rules apply, so
// fields containing commas, quotes, or newlines are escaped automatically.
type CSVFormatter struct{}
// csvHeader is the exact, ordered header row written before any findings.
// Column order must remain stable: downstream consumers may parse by index.
// Columns: id, provider, confidence, key, source, line, detected_at, verified, verify_status
var csvHeader = []string{
"id", "provider", "confidence", "key", "source",
"line", "detected_at", "verified", "verify_status",
}
// Format implements the Formatter interface.
func (CSVFormatter) Format(findings []engine.Finding, w io.Writer, opts Options) error {
cw := csv.NewWriter(w)
if err := cw.Write(csvHeader); err != nil {
return err
}
for i, f := range findings {
key := f.KeyMasked
if opts.Unmask {
key = f.KeyValue
}
row := []string{
strconv.Itoa(i),
f.ProviderName,
f.Confidence,
key,
f.Source,
strconv.Itoa(f.LineNumber),
f.DetectedAt.Format(time.RFC3339),
strconv.FormatBool(f.Verified),
f.VerifyStatus,
}
if err := cw.Write(row); err != nil {
return err
}
}
cw.Flush()
return cw.Error()
}