From cc9dabe5f5ee7d688a17c644478aad6604a65b0a Mon Sep 17 00:00:00 2001 From: salvacybersec Date: Sun, 5 Apr 2026 15:54:51 +0300 Subject: [PATCH] feat(05-05): render VERIFY column and metadata line in output table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - When any finding has Verified=true, append a VERIFY column with colored glyphs: ✓ live / ✗ dead / ⚠ rate / ! err / ? unk - Per-finding VerifyMetadata is rendered on an indented secondary line with deterministic (sorted) key ordering - Backward compatible: unverified scans produce identical output to pre-Phase-5 runs --- pkg/output/table.go | 108 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 91 insertions(+), 17 deletions(-) diff --git a/pkg/output/table.go b/pkg/output/table.go index 219bedb..1dee2d2 100644 --- a/pkg/output/table.go +++ b/pkg/output/table.go @@ -3,34 +3,66 @@ package output import ( "fmt" "os" + "sort" + "strings" "github.com/charmbracelet/lipgloss" "github.com/salvacybersec/keyhunter/pkg/engine" ) var ( - styleHigh = lipgloss.NewStyle().Foreground(lipgloss.Color("2")) // green - styleMedium = lipgloss.NewStyle().Foreground(lipgloss.Color("3")) // yellow - styleLow = lipgloss.NewStyle().Foreground(lipgloss.Color("1")) // red + styleHigh = lipgloss.NewStyle().Foreground(lipgloss.Color("2")) // green + styleMedium = lipgloss.NewStyle().Foreground(lipgloss.Color("3")) // yellow + styleLow = lipgloss.NewStyle().Foreground(lipgloss.Color("1")) // red styleHeader = lipgloss.NewStyle().Bold(true).Underline(true) + + styleVerifyLive = lipgloss.NewStyle().Foreground(lipgloss.Color("2")) // green + styleVerifyDead = lipgloss.NewStyle().Foreground(lipgloss.Color("1")) // red + styleVerifyRate = lipgloss.NewStyle().Foreground(lipgloss.Color("3")) // yellow + styleVerifyErr = lipgloss.NewStyle().Foreground(lipgloss.Color("1")) // red + styleVerifyUnk = lipgloss.NewStyle().Foreground(lipgloss.Color("8")) // gray ) // PrintFindings writes findings as a colored terminal table to stdout. // If unmask is true, KeyValue is shown; otherwise KeyMasked is shown. +// +// When any finding has Verified=true, an extra VERIFY column is appended to +// the table and per-finding VerifyMetadata (if present) is rendered on an +// indented secondary line. Backward compatible: if no finding is verified, +// the layout matches the pre-Phase-5 output exactly. func PrintFindings(findings []engine.Finding, unmask bool) { if len(findings) == 0 { fmt.Println("No API keys found.") return } + anyVerified := false + for _, f := range findings { + if f.Verified { + anyVerified = true + break + } + } + // Header - fmt.Fprintf(os.Stdout, "%-20s %-40s %-10s %-30s %s\n", - styleHeader.Render("PROVIDER"), - styleHeader.Render("KEY"), - styleHeader.Render("CONFIDENCE"), - styleHeader.Render("SOURCE"), - styleHeader.Render("LINE"), - ) + if anyVerified { + fmt.Fprintf(os.Stdout, "%-20s %-40s %-10s %-30s %-5s %s\n", + styleHeader.Render("PROVIDER"), + styleHeader.Render("KEY"), + styleHeader.Render("CONFIDENCE"), + styleHeader.Render("SOURCE"), + styleHeader.Render("LINE"), + styleHeader.Render("VERIFY"), + ) + } else { + fmt.Fprintf(os.Stdout, "%-20s %-40s %-10s %-30s %s\n", + styleHeader.Render("PROVIDER"), + styleHeader.Render("KEY"), + styleHeader.Render("CONFIDENCE"), + styleHeader.Render("SOURCE"), + styleHeader.Render("LINE"), + ) + } fmt.Println(lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Render( "──────────────────────────────────────────────────────────────────────────────────────────────────────────", )) @@ -49,17 +81,59 @@ func PrintFindings(findings []engine.Finding, unmask bool) { confStyle = styleMedium } - fmt.Fprintf(os.Stdout, "%-20s %-40s %-10s %-30s %d\n", - f.ProviderName, - keyDisplay, - confStyle.Render(f.Confidence), - truncate(f.Source, 28), - f.LineNumber, - ) + if anyVerified { + fmt.Fprintf(os.Stdout, "%-20s %-40s %-10s %-30s %-5d %s\n", + f.ProviderName, + keyDisplay, + confStyle.Render(f.Confidence), + truncate(f.Source, 28), + f.LineNumber, + verifySymbol(f), + ) + } else { + fmt.Fprintf(os.Stdout, "%-20s %-40s %-10s %-30s %d\n", + f.ProviderName, + keyDisplay, + confStyle.Render(f.Confidence), + truncate(f.Source, 28), + f.LineNumber, + ) + } + + // Indented metadata line with deterministic key order. + if len(f.VerifyMetadata) > 0 { + parts := make([]string, 0, len(f.VerifyMetadata)) + for k, v := range f.VerifyMetadata { + parts = append(parts, fmt.Sprintf("%s: %s", k, v)) + } + sort.Strings(parts) + fmt.Fprintf(os.Stdout, " ↳ %s\n", strings.Join(parts, ", ")) + } } fmt.Printf("\n%d key(s) found.\n", len(findings)) } +// verifySymbol maps a finding's verification status to a colored glyph. +// Returns an empty string for unverified findings so the VERIFY column is +// simply blank in mixed-result tables. +func verifySymbol(f engine.Finding) string { + if !f.Verified { + return "" + } + switch f.VerifyStatus { + case "live": + return styleVerifyLive.Render("✓ live") + case "dead": + return styleVerifyDead.Render("✗ dead") + case "rate_limited": + return styleVerifyRate.Render("⚠ rate") + case "error": + return styleVerifyErr.Render("! err") + default: + return styleVerifyUnk.Render("? unk") + } +} + func truncate(s string, max int) string { if len(s) <= max { return s