summaryrefslogtreecommitdiffstats
path: root/internal/formatter/breachforum.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/formatter/breachforum.go')
-rw-r--r--internal/formatter/breachforum.go186
1 files changed, 186 insertions, 0 deletions
diff --git a/internal/formatter/breachforum.go b/internal/formatter/breachforum.go
new file mode 100644
index 0000000..6f77fdf
--- /dev/null
+++ b/internal/formatter/breachforum.go
@@ -0,0 +1,186 @@
+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
+ }
+}