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:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user