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 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 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( "──────────────────────────────────────────────────────────────────────────────────────────────────────────", )) for _, f := range findings { keyDisplay := f.KeyMasked if unmask { keyDisplay = f.KeyValue } confStyle := styleLow switch f.Confidence { case "high": confStyle = styleHigh case "medium": confStyle = styleMedium } 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 } return "..." + s[len(s)-max+3:] }