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() }