diff --git a/pkg/output/json.go b/pkg/output/json.go new file mode 100644 index 0000000..9294f25 --- /dev/null +++ b/pkg/output/json.go @@ -0,0 +1,67 @@ +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. +// Empty input produces "[]\n". When Options.Unmask is false the "key" +// field carries the masked representation; "key_masked" always carries +// the masked form regardless of Unmask. Verification fields are omitted +// when empty so unverified scans stay compact. +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"` +} + +// Format implements the Formatter interface. +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) +} diff --git a/pkg/output/json_test.go b/pkg/output/json_test.go index 47dc485..93d1aef 100644 --- a/pkg/output/json_test.go +++ b/pkg/output/json_test.go @@ -142,8 +142,12 @@ func TestJSONFormatter_Indented(t *testing.T) { if err := (JSONFormatter{}).Format([]engine.Finding{f}, &buf, Options{}); err != nil { t.Fatalf("Format returned error: %v", err) } - // Expect 2-space indent on at least one property line. - if !strings.Contains(buf.String(), "\n \"provider\"") { - t.Errorf("output not indented with 2 spaces:\n%s", buf.String()) + // Expect 2-space indent: array items at 2 spaces, nested fields at 4 spaces. + s := buf.String() + if !strings.Contains(s, "\n {") { + t.Errorf("array item not indented with 2 spaces:\n%s", s) + } + if !strings.Contains(s, "\n \"provider\"") { + t.Errorf("nested field not indented with 4 spaces:\n%s", s) } }