package formatter import ( "fmt" "strings" "time" "git.db.org.ai/dborg/internal/models" "git.db.org.ai/dborg/internal/utils" ) func FormatBreachForumResults(response *models.BreachForumSearchResponse, asJSON bool) error { if asJSON { return utils.PrintJSON(response) } PrintSection(fmt.Sprintf("BreachForum Search: %s", Bold(response.Query))) fmt.Printf("%s: %d\n", Dim("Max Hits"), response.MaxHits) if response.Results == nil { PrintWarning("No results found") return nil } results, ok := response.Results.(map[string]any) if !ok { return fmt.Errorf("unexpected results format") } if elapsed, ok := results["elapsed_time_micros"].(float64); ok { fmt.Printf("%s: %s\n", Dim("Search Time"), formatElapsedTime(elapsed)) } if numHits, ok := results["num_hits"].(float64); ok { fmt.Printf("%s: %s\n", Dim("Total Results"), Yellow(fmt.Sprintf("%d", int(numHits)))) } fmt.Println() hits, ok := results["hits"].([]any) if !ok || len(hits) == 0 { PrintWarning("No results found") return nil } for i, hit := range hits { if hitMap, ok := hit.(map[string]any); ok { formatBreachHit(hitMap, i+1, len(hits)) } } if errors, ok := results["errors"].([]any); ok && len(errors) > 0 { fmt.Printf("\n%s\n", Bold(Red("Errors"))) for _, err := range errors { fmt.Printf(" %s %s\n", StatusError.String(), err) } } return nil } func formatBreachHit(hit map[string]any, index, total int) { fmt.Printf("%s %s\n", Gray(fmt.Sprintf("[%d/%d]", index, total)), Bold("Result")) if author, ok := hit["author"].(string); ok && author != "" { cleanAuthor := strings.TrimSpace(strings.TrimPrefix(author, " ")) fmt.Printf(" %s: %s\n", Cyan("Author"), cleanAuthor) } if source, ok := hit["source"].(string); ok && source != "" { sourceColor := getSourceColor(source) fmt.Printf(" %s: %s\n", Cyan("Source"), Colorize(source, sourceColor)) } if hitType, ok := hit["type"].(string); ok && hitType != "" { typeColor := getTypeColor(hitType) fmt.Printf(" %s: %s\n", Cyan("Type"), Colorize(hitType, typeColor)) } if detDate, ok := hit["detection_date"].(string); ok && detDate != "" { formattedDate := formatDetectionDate(detDate) fmt.Printf(" %s: %s\n", Cyan("Detected"), formattedDate) } if content, ok := hit["content"].(string); ok && content != "" { content = strings.TrimSpace(content) if len(content) > 200 { content = TruncateString(content, 200) } lines := strings.Split(content, "\n") fmt.Printf(" %s:\n", Cyan("Content")) for _, line := range lines { if strings.TrimSpace(line) != "" { fmt.Printf(" %s\n", Dim(line)) } } } fmt.Println() } func formatElapsedTime(microseconds float64) string { milliseconds := microseconds / 1000 if milliseconds < 1000 { return fmt.Sprintf("%.2fms", milliseconds) } seconds := milliseconds / 1000 return fmt.Sprintf("%.2fs", seconds) } func formatDetectionDate(dateStr string) string { t, err := time.Parse(time.RFC3339, dateStr) if err != nil { return dateStr } now := time.Now() duration := now.Sub(t) var timeAgo string switch { case duration.Hours() < 1: timeAgo = fmt.Sprintf("%d minutes ago", int(duration.Minutes())) case duration.Hours() < 24: timeAgo = fmt.Sprintf("%d hours ago", int(duration.Hours())) case duration.Hours() < 168: days := int(duration.Hours() / 24) if days == 1 { timeAgo = "1 day ago" } else { timeAgo = fmt.Sprintf("%d days ago", days) } case duration.Hours() < 730: weeks := int(duration.Hours() / 168) if weeks == 1 { timeAgo = "1 week ago" } else { timeAgo = fmt.Sprintf("%d weeks ago", weeks) } default: months := int(duration.Hours() / 730) if months == 1 { timeAgo = "1 month ago" } else { timeAgo = fmt.Sprintf("%d months ago", months) } } formattedDate := t.Format("2006-01-02 15:04") return fmt.Sprintf("%s %s", Yellow(formattedDate), Gray(fmt.Sprintf("(%s)", timeAgo))) } func getSourceColor(source string) string { source = strings.ToLower(source) switch { case strings.Contains(source, "leakbase"): return ColorRed case strings.Contains(source, "blackhat"): return ColorMagenta case strings.Contains(source, "hard-tm"): return ColorYellow case strings.Contains(source, "htdark"): return ColorGray case strings.Contains(source, "crdcrew"): return ColorBlue default: return ColorCyan } } func getTypeColor(hitType string) string { hitType = strings.ToLower(hitType) switch { case strings.Contains(hitType, "credential"): return ColorMagenta case strings.Contains(hitType, "leak"): return ColorRed case strings.Contains(hitType, "breach"): return ColorRed case strings.Contains(hitType, "database"): return ColorYellow default: return ColorCyan } }