feat(05-05): render VERIFY column and metadata line in output table

- 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
This commit is contained in:
salvacybersec
2026-04-05 15:54:51 +03:00
parent edba8fb5d4
commit cc9dabe5f5

View File

@@ -3,34 +3,66 @@ package output
import ( import (
"fmt" "fmt"
"os" "os"
"sort"
"strings"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/salvacybersec/keyhunter/pkg/engine" "github.com/salvacybersec/keyhunter/pkg/engine"
) )
var ( var (
styleHigh = lipgloss.NewStyle().Foreground(lipgloss.Color("2")) // green styleHigh = lipgloss.NewStyle().Foreground(lipgloss.Color("2")) // green
styleMedium = lipgloss.NewStyle().Foreground(lipgloss.Color("3")) // yellow styleMedium = lipgloss.NewStyle().Foreground(lipgloss.Color("3")) // yellow
styleLow = lipgloss.NewStyle().Foreground(lipgloss.Color("1")) // red styleLow = lipgloss.NewStyle().Foreground(lipgloss.Color("1")) // red
styleHeader = lipgloss.NewStyle().Bold(true).Underline(true) 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. // PrintFindings writes findings as a colored terminal table to stdout.
// If unmask is true, KeyValue is shown; otherwise KeyMasked is shown. // 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) { func PrintFindings(findings []engine.Finding, unmask bool) {
if len(findings) == 0 { if len(findings) == 0 {
fmt.Println("No API keys found.") fmt.Println("No API keys found.")
return return
} }
anyVerified := false
for _, f := range findings {
if f.Verified {
anyVerified = true
break
}
}
// Header // Header
fmt.Fprintf(os.Stdout, "%-20s %-40s %-10s %-30s %s\n", if anyVerified {
styleHeader.Render("PROVIDER"), fmt.Fprintf(os.Stdout, "%-20s %-40s %-10s %-30s %-5s %s\n",
styleHeader.Render("KEY"), styleHeader.Render("PROVIDER"),
styleHeader.Render("CONFIDENCE"), styleHeader.Render("KEY"),
styleHeader.Render("SOURCE"), styleHeader.Render("CONFIDENCE"),
styleHeader.Render("LINE"), 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( fmt.Println(lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Render(
"──────────────────────────────────────────────────────────────────────────────────────────────────────────", "──────────────────────────────────────────────────────────────────────────────────────────────────────────",
)) ))
@@ -49,17 +81,59 @@ func PrintFindings(findings []engine.Finding, unmask bool) {
confStyle = styleMedium confStyle = styleMedium
} }
fmt.Fprintf(os.Stdout, "%-20s %-40s %-10s %-30s %d\n", if anyVerified {
f.ProviderName, fmt.Fprintf(os.Stdout, "%-20s %-40s %-10s %-30s %-5d %s\n",
keyDisplay, f.ProviderName,
confStyle.Render(f.Confidence), keyDisplay,
truncate(f.Source, 28), confStyle.Render(f.Confidence),
f.LineNumber, 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)) 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 { func truncate(s string, max int) string {
if len(s) <= max { if len(s) <= max {
return s return s