Files

9.3 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
06-output-reporting 02 execute 1
06-01
pkg/output/json.go
pkg/output/csv.go
pkg/output/json_test.go
pkg/output/csv_test.go
true
OUT-02
OUT-04
truths artifacts key_links
scan results can be rendered as well-formed JSON (one array of finding objects)
scan results can be rendered as CSV with a stable header row
Both formatters honor the Unmask option for KeyValue exposure
Both formatters are registered in output.Registry under 'json' and 'csv'
path provides contains
pkg/output/json.go JSONFormatter implementing Formatter type JSONFormatter struct
path provides contains
pkg/output/csv.go CSVFormatter implementing Formatter type CSVFormatter struct
from to via pattern
pkg/output/json.go pkg/output/formatter.go init() Register("json", JSONFormatter{}) Register("json"
from to via pattern
pkg/output/csv.go pkg/output/formatter.go init() Register("csv", CSVFormatter{}) Register("csv"
Implement JSONFormatter (full Finding serialization) and CSVFormatter (header row + flat rows) so `keyhunter scan --output=json` and `--output=csv` work end to end after Plan 06 wires the scan command.

Purpose: Machine-readable outputs for pipelines and spreadsheets. Addresses OUT-02 and OUT-04. Output: pkg/output/json.go, pkg/output/csv.go, tests.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/phases/06-output-reporting/06-CONTEXT.md @.planning/phases/06-output-reporting/06-01-PLAN.md @pkg/engine/finding.go From Plan 06-01 (pkg/output/formatter.go): ```go type Formatter interface { Format(findings []engine.Finding, w io.Writer, opts Options) error } type Options struct { Unmask bool ToolName string ToolVersion string } func Register(name string, f Formatter) ``` From pkg/engine/finding.go: full Finding struct with Verify* fields. Task 1: JSONFormatter with full finding fields pkg/output/json.go, pkg/output/json_test.go - pkg/output/formatter.go (Formatter interface, Options) - pkg/engine/finding.go - Output is a JSON array: `[{...}, {...}]` with 2-space indent. - Each element includes: provider, key (full when Unmask, masked when not), key_masked, confidence, source, source_type, line, offset, detected_at (RFC3339), verified, verify_status, verify_http_code, verify_metadata, verify_error. - Empty findings slice -> `[]\n`. - Uses encoding/json Encoder with SetIndent("", " "). - Tests: (a) empty slice -> "[]\n"; (b) one finding round-trips through json.Unmarshal with key==KeyMasked when Unmask=false; (c) Unmask=true sets key==KeyValue; (d) verify fields present when Verified=true. Create pkg/output/json.go:
```go
package output

import (
    "encoding/json"
    "io"
    "time"

    "github.com/salvacybersec/keyhunter/pkg/engine"
)

func init() {
    Register("json", JSONFormatter{})
}

// JSONFormatter renders findings as a JSON array with 2-space indent.
type JSONFormatter struct{}

type jsonFinding struct {
    Provider       string            `json:"provider"`
    Key            string            `json:"key"`
    KeyMasked      string            `json:"key_masked"`
    Confidence     string            `json:"confidence"`
    Source         string            `json:"source"`
    SourceType     string            `json:"source_type"`
    Line           int               `json:"line"`
    Offset         int64             `json:"offset"`
    DetectedAt     string            `json:"detected_at"`
    Verified       bool              `json:"verified"`
    VerifyStatus   string            `json:"verify_status,omitempty"`
    VerifyHTTPCode int               `json:"verify_http_code,omitempty"`
    VerifyMetadata map[string]string `json:"verify_metadata,omitempty"`
    VerifyError    string            `json:"verify_error,omitempty"`
}

func (JSONFormatter) Format(findings []engine.Finding, w io.Writer, opts Options) error {
    out := make([]jsonFinding, 0, len(findings))
    for _, f := range findings {
        key := f.KeyMasked
        if opts.Unmask {
            key = f.KeyValue
        }
        out = append(out, jsonFinding{
            Provider:       f.ProviderName,
            Key:            key,
            KeyMasked:      f.KeyMasked,
            Confidence:     f.Confidence,
            Source:         f.Source,
            SourceType:     f.SourceType,
            Line:           f.LineNumber,
            Offset:         f.Offset,
            DetectedAt:     f.DetectedAt.Format(time.RFC3339),
            Verified:       f.Verified,
            VerifyStatus:   f.VerifyStatus,
            VerifyHTTPCode: f.VerifyHTTPCode,
            VerifyMetadata: f.VerifyMetadata,
            VerifyError:    f.VerifyError,
        })
    }
    enc := json.NewEncoder(w)
    enc.SetIndent("", "  ")
    return enc.Encode(out)
}
```

Create pkg/output/json_test.go with tests for empty, masked, unmask, verify fields. Use json.Unmarshal to assert field values.
cd /home/salva/Documents/apikey && go test ./pkg/output/... -run "TestJSONFormatter" -count=1 - TestJSONFormatter_* all pass - `grep -q "Register(\"json\"" pkg/output/json.go` - `go build ./...` succeeds Task 2: CSVFormatter with stable header row pkg/output/csv.go, pkg/output/csv_test.go - pkg/output/formatter.go - pkg/engine/finding.go - Header: id,provider,confidence,key,source,line,detected_at,verified,verify_status - id is the zero-based index within the findings slice (scan-time id; DB id not available here). - key column renders KeyMasked when Unmask=false, KeyValue when Unmask=true. - verified column is "true"/"false". - Uses encoding/csv Writer; flushes on return. - Empty findings still writes header row only. - Tests: header presence; masked vs unmask; CSV quoting of comma in Source; verify_status column populated. Create pkg/output/csv.go:
```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.
type CSVFormatter struct{}

var csvHeader = []string{
    "id", "provider", "confidence", "key", "source",
    "line", "detected_at", "verified", "verify_status",
}

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

Create pkg/output/csv_test.go:
- TestCSVFormatter_HeaderOnly: empty findings -> single header line.
- TestCSVFormatter_Row: one finding, parse with csv.NewReader, assert fields.
- TestCSVFormatter_QuotesCommaInSource: Source="a, b.txt" round-trips via csv reader.
- TestCSVFormatter_Unmask: Unmask=true puts KeyValue into key column.
cd /home/salva/Documents/apikey && go test ./pkg/output/... -run "TestCSVFormatter" -count=1 - TestCSVFormatter_* all pass - `grep -q "Register(\"csv\"" pkg/output/csv.go` - Header exactly matches: `grep -q 'id", "provider", "confidence", "key", "source"' pkg/output/csv.go` - `go build ./...` succeeds - `go test ./pkg/output/... -count=1` all green - Both formats registered: `grep -h "Register(" pkg/output/*.go` shows table, json, csv (sarif added in Plan 03)

<success_criteria>

  • JSONFormatter and CSVFormatter implement Formatter
  • Both are registered on package init
  • Unmask option propagates to key column
  • All unit tests pass </success_criteria>
After completion, create `.planning/phases/06-output-reporting/06-02-SUMMARY.md`.