Updates to allow for enhanced debugging.

Added structs for whois calls.

Added ability to write WhoIs to file.

Added structured output for Whois Records.

Added String Method for WhoIsRecord and WhoIsHistory Records.
This commit is contained in:
Evan Hosinski
2025-05-16 15:33:29 -04:00
parent ef5a8149e1
commit 65c4ea6a15
13 changed files with 1869 additions and 301 deletions
+5 -4
View File
@@ -2,7 +2,7 @@ package cmd
import ( import (
"dehasher/internal/badger" "dehasher/internal/badger"
"dehasher/internal/query" "dehasher/internal/dehashed"
"dehasher/internal/sqlite" "dehasher/internal/sqlite"
"fmt" "fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -12,7 +12,7 @@ func init() {
// Add query command to root command // Add query command to root command
rootCmd.AddCommand(apiCmd) rootCmd.AddCommand(apiCmd)
// Add flags specific to query command // Add flags specific to api command
apiCmd.Flags().IntVarP(&maxRecords, "max-records", "m", 30000, "Maximum amount of records to return") apiCmd.Flags().IntVarP(&maxRecords, "max-records", "m", 30000, "Maximum amount of records to return")
apiCmd.Flags().IntVarP(&maxRequests, "max-requests", "r", -1, "Maximum number of requests to make") apiCmd.Flags().IntVarP(&maxRequests, "max-requests", "r", -1, "Maximum number of requests to make")
apiCmd.Flags().IntVarP(&startingPage, "starting-page", "s", 1, "Starting page for requests") apiCmd.Flags().IntVarP(&startingPage, "starting-page", "s", 1, "Starting page for requests")
@@ -36,7 +36,7 @@ func init() {
apiCmd.Flags().StringVarP(&hashQuery, "hash", "Q", "", "Hashed password query") apiCmd.Flags().StringVarP(&hashQuery, "hash", "Q", "", "Hashed password query")
apiCmd.Flags().StringVarP(&nameQuery, "name", "N", "", "Name query") apiCmd.Flags().StringVarP(&nameQuery, "name", "N", "", "Name query")
// Add mutually exclusive flags to exact match and regex match // Add mutually exclusive flags to wildcard match and regex match
apiCmd.MarkFlagsMutuallyExclusive("regex-match", "wildcard-match") apiCmd.MarkFlagsMutuallyExclusive("regex-match", "wildcard-match")
} }
@@ -103,10 +103,11 @@ var (
wildcardMatch, wildcardMatch,
printBalance, printBalance,
credsOnly, credsOnly,
debugGlobal,
) )
// Create new Dehasher // Create new Dehasher
dehasher := query.NewDehasher(queryOptions) dehasher := dehashed.NewDehasher(queryOptions)
dehasher.SetClientCredentials( dehasher.SetClientCredentials(
key, key,
) )
+3 -5
View File
@@ -11,6 +11,7 @@ import (
var ( var (
// Global Flags // Global Flags
debugGlobal bool
// rootCmd is the base command for the CLI. // rootCmd is the base command for the CLI.
rootCmd = &cobra.Command{ rootCmd = &cobra.Command{
@@ -58,14 +59,11 @@ func Execute() {
} }
func init() { func init() {
//// Attempt to retrieve the useLocalDB
//useLocalDatabase := badger.GetUseLocalDB()
// Hide the default help command // Hide the default help command
rootCmd.CompletionOptions.HiddenDefaultCmd = true rootCmd.CompletionOptions.HiddenDefaultCmd = true
//// Add global flags // Add global flags
//rootCmd.PersistentFlags().BoolVar(&useLocalDatabase, "local-db", useLocalDatabase, "Use local database in current directory instead of default path") rootCmd.PersistentFlags().BoolVar(&debugGlobal, "debug", false, "Show debugGlobal information")
// Add subcommands // Add subcommands
rootCmd.AddCommand(setKeyCmd) rootCmd.AddCommand(setKeyCmd)
+431 -76
View File
@@ -1,14 +1,38 @@
package cmd package cmd
import ( import (
"dehasher/internal/debug"
"dehasher/internal/export"
"dehasher/internal/files"
"dehasher/internal/pretty"
"dehasher/internal/sqlite" "dehasher/internal/sqlite"
"dehasher/internal/whois" "dehasher/internal/whois"
"fmt" "fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"go.uber.org/zap" "go.uber.org/zap"
"strings" "strings"
"time"
) )
func init() {
// Add whois subcommand to root command
rootCmd.AddCommand(whoisCmd)
// Add flags specific to whois command
whoisCmd.Flags().StringVarP(&whoisDomain, "domain", "d", "", "Domain for WHOIS lookup, history search, and subdomain scan")
whoisCmd.Flags().StringVarP(&whoisIPAddress, "ip", "i", "", "IP address for reverse IP lookup")
whoisCmd.Flags().StringVarP(&whoisMXAddress, "mx", "m", "", "MX hostname for reverse MX lookup")
whoisCmd.Flags().StringVarP(&whoisNSAddress, "ns", "n", "", "NS hostname for reverse NS lookup")
whoisCmd.Flags().StringVarP(&whoisInclude, "include", "I", "", "Up to 4 Terms to include in reverse WHOIS search (comma-separated)")
whoisCmd.Flags().StringVarP(&whoisExclude, "exclude", "E", "", "Up to 4 Terms to exclude in reverse WHOIS search (comma-separated)")
whoisCmd.Flags().StringVarP(&whoisReverseType, "type", "t", "registrant", "Type of reverse WHOIS search ([default] current or historic)")
whoisCmd.Flags().StringVarP(&whoisOutputFormat, "format", "f", "text", "Output format (text, json)")
whoisCmd.Flags().StringVarP(&whoisOutputFile, "output", "o", "whois", "File to output results to including extension")
whoisCmd.Flags().BoolVarP(&whoisShowCredits, "credits", "c", false, "Show remaining WHOIS credits")
whoisCmd.Flags().BoolVarP(&whoisHistory, "history", "H", false, "Perform WHOIS history search [25 Credits]")
whoisCmd.Flags().BoolVarP(&whoisSubdomainScan, "subdomains", "s", false, "Perform WHOIS subdomain scan")
}
var ( var (
// WHOIS command flags // WHOIS command flags
whoisDomain string whoisDomain string
@@ -19,6 +43,7 @@ var (
whoisExclude string whoisExclude string
whoisReverseType string whoisReverseType string
whoisOutputFormat string whoisOutputFormat string
whoisOutputFile string
whoisShowCredits bool whoisShowCredits bool
whoisHistory bool whoisHistory bool
whoisSubdomainScan bool whoisSubdomainScan bool
@@ -37,20 +62,50 @@ var (
return return
} }
// Show credits if requested if debugGlobal {
if whoisShowCredits { debug.PrintInfo("debug mode enabled")
fmt.Println("[*] Getting WHOIS credits...") zap.L().Info("whois_debug",
credits, err := whois.GetWHOISCredits(key) zap.String("message", "debug mode enabled"),
if err != nil {
zap.L().Error("get_whois_credits",
zap.String("message", "failed to get whois credits"),
zap.Error(err),
) )
fmt.Printf("Error getting WHOIS credits: %v\n", err) }
if whoisOutputFile == "" {
if debugGlobal {
debug.PrintInfo("output file not specified, using default")
}
whoisOutputFile = "whois_" + time.Now().Format("05_04_05")
}
if whoisOutputFormat == "" {
if debugGlobal {
debug.PrintInfo("output format not specified, using default")
}
whoisOutputFormat = "json"
}
fType := files.GetFileType(whoisOutputFormat)
if fType == files.UNKNOWN {
fmt.Println("[!] Error: Invalid output format. Must be 'json', 'xml', 'yaml', or 'txt'.")
return return
} }
fmt.Printf("WHOIS Credits: %d\n", credits.WhoisCredits) if debugGlobal {
return debug.PrintInfo("using output format: " + whoisOutputFormat)
}
w := whois.NewWhoIs(key, debugGlobal)
// Show credits if requested
if whoisShowCredits {
fmt.Println("[*] Getting WHOIS balance...")
balance, err := w.Balance()
if err != nil {
zap.L().Error("get_whois_credits",
zap.String("message", "failed to get whois balance"),
zap.Error(err),
)
fmt.Printf("Error getting WHOIS balance: %v\n", err)
}
fmt.Printf("WHOIS Credits: %d\n", balance)
} }
// Check if domain is provided for history and subdomain scan // Check if domain is provided for history and subdomain scan
@@ -59,14 +114,30 @@ var (
fmt.Println("Domain is required for history and subdomain scan.") fmt.Println("Domain is required for history and subdomain scan.")
return return
} }
if whoisShowCredits {
balance, err := w.Balance()
if err != nil {
zap.L().Error("get_whois_credits",
zap.String("message", "failed to get whois balance"),
zap.Error(err),
)
fmt.Printf("Error getting WHOIS balance: %v\n", err)
}
fmt.Println("WHOIS Credits: ", balance)
}
} }
// Determine which operation to perform based on flags // Determine which operation to perform based on flags
if whoisDomain != "" { if whoisDomain != "" {
fmt.Println("[*] Performing WHOIS lookup...") fmt.Println("[*] Performing WHOIS lookup...")
// Domain lookup // Domain lookup
result, err := whois.WhoisSearch(whoisDomain, key) result, err := w.WhoisSearch(whoisDomain)
if err != nil { if err != nil {
if debugGlobal {
debug.PrintInfo("failed to perform whois search")
debug.PrintError(err)
}
zap.L().Error("whois_search", zap.L().Error("whois_search",
zap.String("message", "failed to perform whois search"), zap.String("message", "failed to perform whois search"),
zap.Error(err), zap.Error(err),
@@ -75,12 +146,32 @@ var (
return return
} }
if whoisShowCredits {
balance, err := w.Balance()
if err != nil {
if debugGlobal {
debug.PrintInfo("failed to get whois balance")
debug.PrintError(err)
}
zap.L().Error("get_whois_credits",
zap.String("message", "failed to get whois balance"),
zap.Error(err),
)
fmt.Printf("Error getting WHOIS balance: %v\n", err)
}
fmt.Println("WHOIS Credits: ", balance)
}
// Fix the output format to use proper formatting // Fix the output format to use proper formatting
fmt.Printf("WHOIS Lookup Result:\n%+v\n", result.Data.WhoisRecord) fmt.Println("WHOIS Lookup Result:")
// Store the record // Store the record
err = sqlite.StoreWhoisRecord(result.Data.WhoisRecord) err = sqlite.StoreWhoisRecord(result)
if err != nil { if err != nil {
if debugGlobal {
debug.PrintInfo("failed to store whois record")
debug.PrintError(err)
}
zap.L().Error("store_whois_record", zap.L().Error("store_whois_record",
zap.String("message", "failed to store whois record"), zap.String("message", "failed to store whois record"),
zap.Error(err), zap.Error(err),
@@ -89,75 +180,187 @@ var (
// Continue execution even if storage fails // Continue execution even if storage fails
} }
// Pretty Print WhoIs Record
pretty.WhoIsTree(whoisDomain, result)
// Write WhoIs Record to file
if len(result.DomainName) != 0 {
fmt.Printf("[*] Writing WHOIS record to file: %s%s\n", whoisOutputFile, fType.Extension())
err = export.WriteWhoIsRecordToFile(result, whoisOutputFile, fType)
} else {
if debugGlobal {
debug.PrintInfo("no whois record to write to file")
}
zap.L().Info("write_whois_record",
zap.String("message", "no whois record to write to file"),
)
}
if whoisHistory { if whoisHistory {
fmt.Println("[*] Performing WHOIS history search...") fmt.Println("[*] Performing WHOIS history search...")
// Perform history search // Perform history search
history, err := whois.WhoisHistory(whoisDomain, key) historyRecords, err := w.WhoisHistory(whoisDomain)
if err != nil { if err != nil {
if debugGlobal {
debug.PrintInfo("failed to perform whois history lookup")
debug.PrintError(err)
}
zap.L().Error("whois_history", zap.L().Error("whois_history",
zap.String("message", "failed to perform whois history lookup"), zap.String("message", "failed to perform whois history lookup"),
zap.Error(err), zap.Error(err),
) )
fmt.Printf("Error performing WHOIS history lookup: %v\n", err) fmt.Printf("[!] Error performing WHOIS history lookup: %v\n", err)
} else { } else {
fmt.Println("\nWHOIS History:") if whoisShowCredits {
fmt.Println(history) balance, err := w.Balance()
if err != nil {
if debugGlobal {
debug.PrintInfo("failed to get whois balance")
debug.PrintError(err)
}
zap.L().Error("get_whois_credits",
zap.String("message", "failed to get whois balance"),
zap.Error(err),
)
fmt.Printf("Error getting WHOIS balance: %v\n", err)
}
fmt.Println("WHOIS Credits: ", balance)
} }
err = sqlite.StoreHistoryRecord(history.Data.Records) // Write history records to file if any
if len(historyRecords) > 0 {
fmt.Println("[*] Records Found: %d\n", len(historyRecords))
fmt.Println("[*] WHOIS History being written to file: %s%s\n", whoisOutputFile, fType.Extension())
writeErr := export.WriteWhoIsHistoryToFile(historyRecords, whoisOutputFile, fType)
if writeErr != nil {
if debugGlobal {
debug.PrintInfo("failed to write whois history to file")
debug.PrintError(writeErr)
}
zap.L().Error("write_whois_history",
zap.String("message", "failed to write whois history to file"),
zap.Error(writeErr),
)
fmt.Printf("[!] Error writing WHOIS history to file: %v\n", writeErr)
}
err = sqlite.StoreHistoryRecord(historyRecords)
if err != nil { if err != nil {
if debugGlobal {
debug.PrintInfo("failed to store history record")
debug.PrintError(err)
}
zap.L().Error("store_history_record", zap.L().Error("store_history_record",
zap.String("message", "failed to store history record"), zap.String("message", "failed to store history record"),
zap.Error(err), zap.Error(err),
) )
fmt.Printf("Error storing WHOIS history record: %v\n", err) fmt.Printf("Error storing WHOIS history record: %v\n", err)
} }
} else {
if debugGlobal {
debug.PrintInfo("no whois history records to write to file")
}
zap.L().Info("write_whois_history",
zap.String("message", "no whois history records to write to file"),
)
}
}
} }
// Perform subdomain scan // Perform subdomain scan
if whoisSubdomainScan { if whoisSubdomainScan {
fmt.Println("[*] Performing WHOIS subdomain scan...") fmt.Println("[*] Performing WHOIS subdomain scan...")
subdomains, err := whois.WhoisSubdomainScan(whoisDomain, key) subdomains, err := w.WhoisSubdomainScan(whoisDomain)
if whoisShowCredits {
balance, err := w.Balance()
if err != nil { if err != nil {
if debugGlobal {
debug.PrintInfo("failed to get whois balance")
debug.PrintError(err)
}
zap.L().Error("get_whois_credits",
zap.String("message", "failed to get whois balance"),
zap.Error(err),
)
fmt.Printf("Error getting WHOIS balance: %v\n", err)
}
fmt.Println("WHOIS Credits: ", balance)
}
if err != nil {
if debugGlobal {
debug.PrintInfo("failed to perform subdomain scan")
debug.PrintError(err)
}
zap.L().Error("whois_subdomain_scan", zap.L().Error("whois_subdomain_scan",
zap.String("message", "failed to perform subdomain scan"), zap.String("message", "failed to perform subdomain scan"),
zap.Error(err), zap.Error(err),
) )
fmt.Printf("Error performing subdomain scan: %v\n", err) fmt.Printf("Error performing subdomain scan: %v\n", err)
} else { } else {
fmt.Println("\nSubdomain Scan:") fmt.Println("Subdomain Scan:")
fmt.Println(subdomains) err = sqlite.StoreSubdomainRecords(subdomains)
}
err = sqlite.StoreSubdomainRecord(subdomains.Data.Result.Records)
if err != nil { if err != nil {
if debugGlobal {
debug.PrintInfo("failed to store subdomain record")
debug.PrintError(err)
}
zap.L().Error("store_subdomain_record", zap.L().Error("store_subdomain_record",
zap.String("message", "failed to store subdomain record"), zap.String("message", "failed to store subdomain record"),
zap.Error(err), zap.Error(err),
) )
fmt.Printf("Error storing WHOIS subdomain record: %v\n", err) fmt.Printf("Error storing WHOIS subdomain record: %v\n", err)
} }
}
// Get credits // Write the subdomains to file if any
credits, err := whois.GetWHOISCredits(key) if len(subdomains) > 0 {
fmt.Printf("[*] Writing subdomains to file: %s%s\n", whoisOutputFile, fType.Extension())
err = export.WriteSubdomainsToFile(subdomains, whoisOutputFile, fType)
if err != nil { if err != nil {
zap.L().Error("get_whois_credits", zap.L().Error("write_whois_subdomain",
zap.String("message", "failed to get whois credits"), zap.String("message", "failed to write whois subdomain to file"),
zap.Error(err), zap.Error(err),
) )
fmt.Printf("Error getting WHOIS credits: %v\n", err) fmt.Printf("Error writing WHOIS subdomain to file: %v\n", err)
return
} }
fmt.Printf("\nWHOIS Credits Remaining: %d\n", credits.WhoisCredits)
var (
headers = []string{"Domain", "First Seen", "Last Seen"}
rows [][]string
)
// Create the rows
for _, r := range subdomains {
rows = append(rows, []string{r.Domain, time.Unix(r.FirstSeen, 0).String(), time.Unix(r.LastSeen, 0).String()})
}
// Store the subdomains
pretty.Table(headers, rows)
} else {
fmt.Println("[!] No subdomains found")
zap.L().Info("write_whois_subdomain",
zap.String("message", "no whois subdomain records to write to file"),
zap.String("domain", whoisDomain),
)
}
}
}
return return
} }
if whoisIPAddress != "" { if whoisIPAddress != "" {
fmt.Println("[*] Performing reverse IP lookup...") fmt.Println("[*] Performing reverse IP lookup...")
// IP lookup // IP lookup
result, err := whois.WhoisIP(whoisIPAddress, key) result, err := w.WhoisIP(whoisIPAddress)
if err != nil { if err != nil {
if debugGlobal {
debug.PrintInfo("failed to perform ip lookup")
debug.PrintError(err)
}
zap.L().Error("whois_ip", zap.L().Error("whois_ip",
zap.String("message", "failed to perform ip lookup"), zap.String("message", "failed to perform ip lookup"),
zap.Error(err), zap.Error(err),
@@ -165,28 +368,73 @@ var (
fmt.Printf("Error performing IP lookup: %v\n", err) fmt.Printf("Error performing IP lookup: %v\n", err)
return return
} }
fmt.Println("IP Lookup Result:")
fmt.Println(string(result))
// Get credits // Get credits
credits, err := whois.GetWHOISCredits(key) if whoisShowCredits {
balance, err := w.Balance()
if err != nil { if err != nil {
if debugGlobal {
debug.PrintInfo("failed to get whois balance")
debug.PrintError(err)
}
zap.L().Error("get_whois_credits", zap.L().Error("get_whois_credits",
zap.String("message", "failed to get whois credits"), zap.String("message", "failed to get whois balance"),
zap.Error(err), zap.Error(err),
) )
fmt.Printf("Error getting WHOIS credits: %v\n", err) fmt.Printf("Error getting WHOIS balance: %v\n", err)
}
fmt.Println("WHOIS Credits: ", balance)
}
if len(result) == 0 {
fmt.Println("[!] No results found")
zap.L().Info("whois_ip",
zap.String("message", "no results found"),
zap.String("ip", whoisIPAddress),
)
return return
} }
fmt.Printf("\nWHOIS Credits Remaining: %d\n", credits.WhoisCredits)
// Write the results to file
fmt.Printf("[*] Writing IP lookup results to file: %s%s\n", whoisOutputFile, fType.Extension())
err = export.WriteIPLookupToFile(result, whoisOutputFile, fType)
if err != nil {
if debugGlobal {
debug.PrintInfo("failed to write ip lookup to file")
debug.PrintError(err)
}
zap.L().Error("write_ip_lookup",
zap.String("message", "failed to write ip lookup to file"),
zap.Error(err),
)
fmt.Printf("Error writing IP lookup to file: %v\n", err)
}
// Pretty Print the JSON
var (
headers = []string{"Name", "Search Term", "First Seen", "Last Visit", "Type"}
rows [][]string
)
fmt.Println("IP Lookup Result:")
for _, r := range result {
rows = append(rows, []string{r.Name, r.SearchTerm, time.Unix(r.FirstSeen, 0).String(), time.Unix(r.LastVisit, 0).String(), r.Type})
}
pretty.Table(headers, rows)
return return
} }
if whoisMXAddress != "" { if whoisMXAddress != "" {
fmt.Println("[*] Performing reverse MX lookup...") fmt.Println("[*] Performing reverse MX lookup...")
// MX lookup // MX lookup
result, err := whois.WhoisMX(whoisMXAddress, key) result, err := w.WhoisMX(whoisMXAddress)
if err != nil { if err != nil {
if debugGlobal {
debug.PrintInfo("failed to perform mx lookup")
debug.PrintError(err)
}
zap.L().Error("whois_mx", zap.L().Error("whois_mx",
zap.String("message", "failed to perform mx lookup"), zap.String("message", "failed to perform mx lookup"),
zap.Error(err), zap.Error(err),
@@ -195,29 +443,72 @@ var (
return return
} }
// todo unmarshal mx lookup
fmt.Println("MX Lookup Result:")
fmt.Println(result)
// Get credits // Get credits
credits, err := whois.GetWHOISCredits(key) if whoisShowCredits {
balance, err := w.Balance()
if err != nil { if err != nil {
if debugGlobal {
debug.PrintInfo("failed to get whois balance")
debug.PrintError(err)
}
zap.L().Error("get_whois_credits", zap.L().Error("get_whois_credits",
zap.String("message", "failed to get whois credits"), zap.String("message", "failed to get whois balance"),
zap.Error(err), zap.Error(err),
) )
fmt.Printf("Error getting WHOIS credits: %v\n", err) fmt.Printf("Error getting WHOIS balance: %v\n", err)
}
fmt.Println("WHOIS Credits: ", balance)
}
if len(result) == 0 {
fmt.Println("[!] No results found")
zap.L().Info("whois_mx",
zap.String("message", "no results found"),
zap.String("mx", whoisMXAddress),
)
return return
} }
fmt.Printf("\nWHOIS Credits Remaining: %d\n", credits.WhoisCredits)
// Write the results to file
fmt.Printf("[*] Writing MX lookup results to file: %s%s\n", whoisOutputFile, fType.Extension())
err = export.WriteIPLookupToFile(result, whoisOutputFile, fType)
if err != nil {
if debugGlobal {
debug.PrintInfo("failed to write mx lookup to file")
debug.PrintError(err)
}
zap.L().Error("write_mx_lookup",
zap.String("message", "failed to write mx lookup to file"),
zap.Error(err),
)
fmt.Printf("Error writing MX lookup to file: %v\n", err)
}
// Pretty Print the JSON
var (
headers = []string{"Name", "Search Term", "First Seen", "Last Visit", "Type"}
rows [][]string
)
fmt.Println("MX Lookup Result:")
for _, r := range result {
rows = append(rows, []string{r.Name, r.SearchTerm, time.Unix(r.FirstSeen, 0).String(), time.Unix(r.LastVisit, 0).String(), r.Type})
}
pretty.Table(headers, rows)
return return
} }
if whoisNSAddress != "" { if whoisNSAddress != "" {
fmt.Println("[*] Performing reverse NS lookup...") fmt.Println("[*] Performing reverse NS lookup...")
// NS lookup // NS lookup
result, err := whois.WhoisNS(whoisNSAddress, key) result, err := w.WhoisNS(whoisNSAddress)
if err != nil { if err != nil {
if debugGlobal {
debug.PrintInfo("failed to perform ns lookup")
debug.PrintError(err)
}
zap.L().Error("whois_ns", zap.L().Error("whois_ns",
zap.String("message", "failed to perform ns lookup"), zap.String("message", "failed to perform ns lookup"),
zap.Error(err), zap.Error(err),
@@ -225,20 +516,61 @@ var (
fmt.Printf("Error performing NS lookup: %v\n", err) fmt.Printf("Error performing NS lookup: %v\n", err)
return return
} }
fmt.Println("NS Lookup Result:")
fmt.Println(result)
// Get credits // Get credits
credits, err := whois.GetWHOISCredits(key) if whoisShowCredits {
balance, err := w.Balance()
if err != nil { if err != nil {
if debugGlobal {
debug.PrintInfo("failed to get whois balance")
debug.PrintError(err)
}
zap.L().Error("get_whois_credits", zap.L().Error("get_whois_credits",
zap.String("message", "failed to get whois credits"), zap.String("message", "failed to get whois balance"),
zap.Error(err), zap.Error(err),
) )
fmt.Printf("Error getting WHOIS credits: %v\n", err) fmt.Printf("Error getting WHOIS balance: %v\n", err)
}
fmt.Println("WHOIS Credits: ", balance)
}
if len(result) == 0 {
fmt.Println("[!] No results found")
zap.L().Info("whois_ns",
zap.String("message", "no results found"),
zap.String("ns", whoisNSAddress),
)
return return
} }
fmt.Printf("\nWHOIS Credits Remaining: %d\n", credits.WhoisCredits)
// Write the results to file
fmt.Printf("[*] Writing NS lookup results to file: %s%s\n", whoisOutputFile, fType.Extension())
err = export.WriteIPLookupToFile(result, whoisOutputFile, fType)
if err != nil {
if debugGlobal {
debug.PrintInfo("failed to write ns lookup to file")
debug.PrintError(err)
}
zap.L().Error("write_ns_lookup",
zap.String("message", "failed to write ns lookup to file"),
zap.Error(err),
)
fmt.Printf("Error writing NS lookup to file: %v\n", err)
}
// Pretty Print the JSON
var (
headers = []string{"Name", "Search Term", "First Seen", "Last Visit", "Type"}
rows [][]string
)
fmt.Println("NS Lookup Result:")
for _, r := range result {
rows = append(rows, []string{r.Name, r.SearchTerm, time.Unix(r.FirstSeen, 0).String(), time.Unix(r.LastVisit, 0).String(), r.Type})
}
pretty.Table(headers, rows)
return return
} }
@@ -247,25 +579,66 @@ var (
includeTerms := []string{} includeTerms := []string{}
if whoisInclude != "" { if whoisInclude != "" {
includeTerms = strings.Split(whoisInclude, ",") includeTerms = strings.Split(whoisInclude, ",")
if len(includeTerms) > 4 {
fmt.Println("[!] Error: Maximum of 4 include terms allowed.")
return
}
} }
excludeTerms := []string{} excludeTerms := []string{}
if whoisExclude != "" { if whoisExclude != "" {
excludeTerms = strings.Split(whoisExclude, ",") excludeTerms = strings.Split(whoisExclude, ",")
if len(excludeTerms) > 4 {
fmt.Println("[!] Error: Maximum of 4 exclude terms allowed.")
return
}
} }
if whoisReverseType == "" { if whoisReverseType == "" {
whoisReverseType = "registrant" if debugGlobal {
debug.PrintInfo("reverse type not specified, using default")
}
whoisReverseType = "current"
} else {
toLower := strings.ToLower(whoisReverseType)
if toLower != "current" && toLower != "historic" {
fmt.Println("[!] Error: Invalid reverse type. Must be 'current' or 'historic'.")
return
}
} }
fmt.Println("[*] Performing reverse WHOIS lookup...") fmt.Println("[*] Performing reverse WHOIS lookup...")
result, err := whois.ReverseWHOIS(includeTerms, excludeTerms, whoisReverseType, key) result, err := w.ReverseWHOIS(includeTerms, excludeTerms, whoisReverseType)
if err != nil { if err != nil {
if debugGlobal {
debug.PrintInfo("failed to perform reverse whois")
debug.PrintError(err)
}
zap.L().Error("reverse_whois",
zap.String("message", "failed to perform reverse whois"),
zap.Error(err),
)
fmt.Printf("Error performing reverse WHOIS: %v\n", err) fmt.Printf("Error performing reverse WHOIS: %v\n", err)
return return
} }
fmt.Println("Reverse WHOIS Result:") fmt.Println("Reverse WHOIS Result:")
fmt.Println(result) fmt.Println(result)
if whoisShowCredits {
balance, err := w.Balance()
if err != nil {
if debugGlobal {
debug.PrintInfo("failed to get whois balance")
debug.PrintError(err)
}
zap.L().Error("get_whois_credits",
zap.String("message", "failed to get whois balance"),
zap.Error(err),
)
fmt.Printf("Error getting WHOIS balance: %v\n", err)
}
fmt.Println("WHOIS Credits: ", balance)
}
return return
} }
@@ -274,21 +647,3 @@ var (
}, },
} }
) )
func init() {
// Add whois command to root command
rootCmd.AddCommand(whoisCmd)
// Add flags specific to whois command
whoisCmd.Flags().StringVarP(&whoisDomain, "domain", "d", "", "Domain for WHOIS lookup, history search, and subdomain scan")
whoisCmd.Flags().StringVarP(&whoisIPAddress, "ip", "i", "", "IP address for reverse IP lookup")
whoisCmd.Flags().StringVarP(&whoisMXAddress, "mx", "m", "", "MX address for reverse MX lookup")
whoisCmd.Flags().StringVarP(&whoisNSAddress, "ns", "n", "", "NS address for reverse NS lookup")
whoisCmd.Flags().StringVarP(&whoisInclude, "include", "I", "", "Terms to include in reverse WHOIS search (comma-separated)")
whoisCmd.Flags().StringVarP(&whoisExclude, "exclude", "E", "", "Terms to exclude in reverse WHOIS search (comma-separated)")
whoisCmd.Flags().StringVarP(&whoisReverseType, "type", "t", "registrant", "Type of reverse WHOIS search (registrant, email, organization, address, phone)")
whoisCmd.Flags().StringVarP(&whoisOutputFormat, "format", "f", "text", "Output format (text, json)")
whoisCmd.Flags().BoolVarP(&whoisShowCredits, "credits", "c", false, "Show remaining WHOIS credits")
whoisCmd.Flags().BoolVarP(&whoisHistory, "history", "H", false, "Perform WHOIS history search [25 Credits]")
whoisCmd.Flags().BoolVarP(&whoisSubdomainScan, "subdomains", "s", false, "Perform WHOIS subdomain scan")
}
@@ -1,8 +1,9 @@
package query package dehashed
import ( import (
"bytes" "bytes"
"crypto/sha256" "crypto/sha256"
"dehasher/internal/debug"
"dehasher/internal/sqlite" "dehasher/internal/sqlite"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
@@ -38,6 +39,7 @@ func (dp DehashedParameter) GetArgumentString(arg string) string {
type DehashedSearchRequest struct { type DehashedSearchRequest struct {
ForcePlaintext bool `json:"-"` ForcePlaintext bool `json:"-"`
Debug bool `json:"-"`
Page int `json:"page"` Page int `json:"page"`
Query string `json:"query"` Query string `json:"query"`
Size int `json:"size"` Size int `json:"size"`
@@ -46,42 +48,60 @@ type DehashedSearchRequest struct {
DeDupe bool `json:"de_dupe"` DeDupe bool `json:"de_dupe"`
} }
func NewDehashedSearchRequest(page, size int, wildcard, regex, forcePlaintext bool) *DehashedSearchRequest { func NewDehashedSearchRequest(page, size int, wildcard, regex, forcePlaintext, debug bool) *DehashedSearchRequest {
return &DehashedSearchRequest{Page: page, Query: "", Size: size, Wildcard: wildcard, Regex: regex, DeDupe: true, ForcePlaintext: forcePlaintext} return &DehashedSearchRequest{Page: page, Query: "", Size: size, Wildcard: wildcard, Regex: regex, DeDupe: true, ForcePlaintext: forcePlaintext, Debug: debug}
} }
func (dsr *DehashedSearchRequest) buildQuery(query string, param DehashedParameter) { func (dsr *DehashedSearchRequest) buildQuery(query string) {
if dsr.Debug {
debug.PrintInfo(fmt.Sprintf("building query: %s", query))
}
// Ensure query is properly formatted
query = strings.TrimSpace(query)
// For regex queries, we need to ensure the regex pattern is properly escaped
// and not enquoted, as that would break the regex pattern
if dsr.Regex && !strings.HasPrefix(query, "\"") && !strings.HasSuffix(query, "\"") {
// Don't add extra quotes for regex patterns
} else if strings.Contains(query, " ") && !strings.HasPrefix(query, "\"") {
query = fmt.Sprintf("\"%s\"", query)
}
if len(dsr.Query) > 0 { if len(dsr.Query) > 0 {
dsr.Query = fmt.Sprintf("%s&%s", strings.TrimSpace(dsr.Query), strings.TrimSpace(query)) dsr.Query = fmt.Sprintf("%s&%s", strings.TrimSpace(dsr.Query), query)
} else { } else {
dsr.Query = query dsr.Query = query
} }
if dsr.Debug {
debug.PrintInfo(fmt.Sprintf("query built: %s", dsr.Query))
}
} }
func (dsr *DehashedSearchRequest) AddUsernameQuery(query string) { func (dsr *DehashedSearchRequest) AddUsernameQuery(query string) {
query = enquoteSpaced(query) query = enquoteSpaced(query)
dsr.buildQuery(Username.GetArgumentString(query), Username) dsr.buildQuery(Username.GetArgumentString(query))
} }
func (dsr *DehashedSearchRequest) AddEmailQuery(query string) { func (dsr *DehashedSearchRequest) AddEmailQuery(query string) {
query = enquoteSpaced(query) query = enquoteSpaced(query)
dsr.buildQuery(Email.GetArgumentString(query), Email) dsr.buildQuery(Email.GetArgumentString(query))
} }
func (dsr *DehashedSearchRequest) AddIpAddressQuery(query string) { func (dsr *DehashedSearchRequest) AddIpAddressQuery(query string) {
query = enquoteSpaced(query) query = enquoteSpaced(query)
dsr.buildQuery(IpAddress.GetArgumentString(query), IpAddress) dsr.buildQuery(IpAddress.GetArgumentString(query))
} }
func (dsr *DehashedSearchRequest) AddDomainQuery(query string) { func (dsr *DehashedSearchRequest) AddDomainQuery(query string) {
query = enquoteSpaced(query) query = enquoteSpaced(query)
dsr.buildQuery(Domain.GetArgumentString(query), Domain) dsr.buildQuery(Domain.GetArgumentString(query))
} }
func (dsr *DehashedSearchRequest) AddPasswordQuery(query string) { func (dsr *DehashedSearchRequest) AddPasswordQuery(query string) {
if dsr.ForcePlaintext { if dsr.ForcePlaintext {
query = enquoteSpaced(query) query = enquoteSpaced(query)
dsr.buildQuery(Password.GetArgumentString(query), Password) dsr.buildQuery(Password.GetArgumentString(query))
return return
} }
hash := sha256.Sum256([]byte(query)) hash := sha256.Sum256([]byte(query))
@@ -91,89 +111,126 @@ func (dsr *DehashedSearchRequest) AddPasswordQuery(query string) {
func (dsr *DehashedSearchRequest) AddVinQuery(query string) { func (dsr *DehashedSearchRequest) AddVinQuery(query string) {
query = enquoteSpaced(query) query = enquoteSpaced(query)
dsr.buildQuery(Vin.GetArgumentString(query), Vin) dsr.buildQuery(Vin.GetArgumentString(query))
} }
func (dsr *DehashedSearchRequest) AddLicensePlateQuery(query string) { func (dsr *DehashedSearchRequest) AddLicensePlateQuery(query string) {
query = enquoteSpaced(query) query = enquoteSpaced(query)
dsr.buildQuery(LicensePlate.GetArgumentString(query), LicensePlate) dsr.buildQuery(LicensePlate.GetArgumentString(query))
} }
func (dsr *DehashedSearchRequest) AddAddressQuery(query string) { func (dsr *DehashedSearchRequest) AddAddressQuery(query string) {
query = enquoteSpaced(query) query = enquoteSpaced(query)
dsr.buildQuery(Address.GetArgumentString(query), Address) dsr.buildQuery(Address.GetArgumentString(query))
} }
func (dsr *DehashedSearchRequest) AddPhoneQuery(query string) { func (dsr *DehashedSearchRequest) AddPhoneQuery(query string) {
query = enquoteSpaced(query) query = enquoteSpaced(query)
dsr.buildQuery(Phone.GetArgumentString(query), Phone) dsr.buildQuery(Phone.GetArgumentString(query))
} }
func (dsr *DehashedSearchRequest) AddSocialQuery(query string) { func (dsr *DehashedSearchRequest) AddSocialQuery(query string) {
query = enquoteSpaced(query) query = enquoteSpaced(query)
dsr.buildQuery(Social.GetArgumentString(query), Social) dsr.buildQuery(Social.GetArgumentString(query))
} }
func (dsr *DehashedSearchRequest) AddCryptoAddressQuery(query string) { func (dsr *DehashedSearchRequest) AddCryptoAddressQuery(query string) {
query = enquoteSpaced(query) query = enquoteSpaced(query)
dsr.buildQuery(CryptoAddress.GetArgumentString(query), CryptoAddress) dsr.buildQuery(CryptoAddress.GetArgumentString(query))
} }
func (dsr *DehashedSearchRequest) AddHashedPasswordQuery(query string) { func (dsr *DehashedSearchRequest) AddHashedPasswordQuery(query string) {
query = strings.TrimSpace(query) query = strings.TrimSpace(query)
dsr.buildQuery(HashedPassword.GetArgumentString(query), HashedPassword) dsr.buildQuery(HashedPassword.GetArgumentString(query))
} }
func (dsr *DehashedSearchRequest) AddNameQuery(query string) { func (dsr *DehashedSearchRequest) AddNameQuery(query string) {
query = enquoteSpaced(query) query = enquoteSpaced(query)
dsr.buildQuery(Name.GetArgumentString(query), Name) dsr.buildQuery(Name.GetArgumentString(query))
} }
type DehashedClientV2 struct { type DehashedClientV2 struct {
apiKey string apiKey string
results []sqlite.Result results []sqlite.Result
debug bool
} }
func NewDehashedClientV2(apiKey string) *DehashedClientV2 { func NewDehashedClientV2(apiKey string, debug bool) *DehashedClientV2 {
return &DehashedClientV2{apiKey: apiKey} return &DehashedClientV2{apiKey: apiKey, debug: debug}
} }
func (dcv2 *DehashedClientV2) Search(searchRequest DehashedSearchRequest) (int, error) { func (dcv2 *DehashedClientV2) Search(searchRequest DehashedSearchRequest) (int, int, error) {
if dcv2.debug {
debug.PrintInfo("preparing search request")
zap.L().Info("v2_search_debug",
zap.String("message", "preparing search request"),
)
}
reqBody, _ := json.Marshal(searchRequest) reqBody, _ := json.Marshal(searchRequest)
if dcv2.debug {
j := string(reqBody)
jReq := fmt.Sprintf("Request Body: %s\n", j)
debug.PrintJson(jReq)
zap.L().Info("v2_search_debug",
zap.String("message", jReq),
zap.String("body", j),
)
}
req, err := http.NewRequest("POST", "https://api.dehashed.com/v2/search", bytes.NewReader(reqBody)) req, err := http.NewRequest("POST", "https://api.dehashed.com/v2/search", bytes.NewReader(reqBody))
if err != nil { if err != nil {
return -1, err return -1, -1, err
} }
if dcv2.debug {
debug.PrintInfo("setting headers")
zap.L().Info("v2_search_debug",
zap.String("message", "setting headers"),
)
}
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
req.Header.Set("Dehashed-Api-Key", dcv2.apiKey) req.Header.Set("Dehashed-Api-Key", dcv2.apiKey)
if dcv2.debug {
headers := req.Header.Clone()
h := fmt.Sprintf("Headers: %v\n", headers)
debug.PrintJson(h)
zap.L().Info("v2_search_debug",
zap.String("message", h),
zap.String("headers", fmt.Sprintf("%v", headers)),
)
debug.PrintInfo("performing request")
zap.L().Info("v2_search_debug",
zap.String("message", "performing request"),
)
}
res, err := http.DefaultClient.Do(req) res, err := http.DefaultClient.Do(req)
if res != nil { if res != nil {
defer res.Body.Close() defer res.Body.Close()
} }
if err != nil { if err != nil {
if dcv2.debug {
debug.PrintInfo("failed to perform request")
debug.PrintError(err)
}
zap.L().Error("v2_search", zap.L().Error("v2_search",
zap.String("message", "failed to perform request"), zap.String("message", "failed to perform request"),
zap.Error(err), zap.Error(err),
) )
return -1, err return -1, -1, err
} }
if res == nil { if res == nil {
if dcv2.debug {
debug.PrintInfo("response was nil")
}
zap.L().Error("v2_search", zap.L().Error("v2_search",
zap.String("message", "response was nil"), zap.String("message", "response was nil"),
) )
return -1, errors.New("response was nil") return -1, -1, errors.New("response was nil")
}
// Check for HTTP status code errors
if res.StatusCode != 200 {
dhErr := GetDehashedError(res.StatusCode)
fmt.Printf("[%d] API Error message: %s\n", res.StatusCode, dhErr.Error())
zap.L().Error("v2_search",
zap.String("message", "received error status code"),
zap.Int("status_code", res.StatusCode),
zap.String("error", dhErr.Error()),
)
return -1, &dhErr
} }
b, err := io.ReadAll(res.Body) b, err := io.ReadAll(res.Body)
@@ -182,21 +239,50 @@ func (dcv2 *DehashedClientV2) Search(searchRequest DehashedSearchRequest) (int,
zap.String("message", "failed to read response body"), zap.String("message", "failed to read response body"),
zap.Error(err), zap.Error(err),
) )
return -1, err return -1, -1, err
}
// Check for HTTP status code errors
if res.StatusCode != 200 {
if dcv2.debug {
debug.PrintInfo("received error status code")
debug.PrintJson(fmt.Sprintf("Status Code: %d\n", res.StatusCode))
debug.PrintJson(fmt.Sprintf("Body: %s\n", string(b[:])))
}
dhErr := GetDehashedError(res.StatusCode)
zap.L().Error("v2_search",
zap.String("message", "received error status code"),
zap.Int("status_code", res.StatusCode),
zap.String("error", dhErr.Error()),
zap.String("body_error", string(b)),
)
return -1, -1, &dhErr
} }
var responseResults sqlite.DehashedResponse var responseResults sqlite.DehashedResponse
err = json.Unmarshal(b, &responseResults) err = json.Unmarshal(b, &responseResults)
if err != nil { if err != nil {
if dcv2.debug {
debug.PrintInfo("failed to unmarshal response body")
debug.PrintError(err)
}
zap.L().Error("v2_search", zap.L().Error("v2_search",
zap.String("message", "failed to unmarshal response body"), zap.String("message", "failed to unmarshal response body"),
zap.Error(err), zap.Error(err),
) )
return -1, err return -1, -1, err
}
if dcv2.debug {
debug.PrintInfo("appending results")
debug.PrintJson(fmt.Sprintf("Total Results: %d\n", responseResults.TotalResults))
debug.PrintJson(fmt.Sprintf("Balance: %d\n", responseResults.Balance))
debug.PrintJson(fmt.Sprintf("Entries: %d\n", len(responseResults.Entries)))
} }
dcv2.results = append(dcv2.results, responseResults.Entries...) dcv2.results = append(dcv2.results, responseResults.Entries...)
return responseResults.TotalResults, nil return responseResults.TotalResults, responseResults.Balance, nil
} }
func (dcv2 *DehashedClientV2) GetResults() sqlite.DehashedResults { func (dcv2 *DehashedClientV2) GetResults() sqlite.DehashedResults {
@@ -1,6 +1,7 @@
package query package dehashed
import ( import (
"dehasher/internal/debug"
"dehasher/internal/export" "dehasher/internal/export"
"dehasher/internal/sqlite" "dehasher/internal/sqlite"
"encoding/json" "encoding/json"
@@ -13,6 +14,8 @@ import (
type Dehasher struct { type Dehasher struct {
options sqlite.QueryOptions options sqlite.QueryOptions
nextPage int nextPage int
debug bool
balance int
request *DehashedSearchRequest request *DehashedSearchRequest
client *DehashedClientV2 client *DehashedClientV2
} }
@@ -22,19 +25,24 @@ func NewDehasher(options *sqlite.QueryOptions) *Dehasher {
dh := &Dehasher{ dh := &Dehasher{
options: *options, options: *options,
nextPage: options.StartingPage + 1, nextPage: options.StartingPage + 1,
debug: options.Debug,
balance: 0,
} }
dh.setQueries() dh.setQueries()
dh.request = NewDehashedSearchRequest(dh.options.StartingPage, dh.options.MaxRecords, dh.options.WildcardMatch, dh.options.RegexMatch, false) dh.request = NewDehashedSearchRequest(dh.options.StartingPage, dh.options.MaxRecords, dh.options.WildcardMatch, dh.options.RegexMatch, false, options.Debug)
dh.buildRequest() dh.buildRequest()
return dh return dh
} }
// SetClientCredentials sets the client credentials for the dehasher // SetClientCredentials sets the client credentials for the dehasher
func (dh *Dehasher) SetClientCredentials(key string) { func (dh *Dehasher) SetClientCredentials(key string) {
dh.client = NewDehashedClientV2(key) dh.client = NewDehashedClientV2(key, dh.debug)
} }
func (dh *Dehasher) getNextPage() int { func (dh *Dehasher) getNextPage() int {
if dh.debug {
debug.PrintInfo(fmt.Sprintf("getting next page: %d", dh.nextPage))
}
nextPage := dh.nextPage nextPage := dh.nextPage
dh.nextPage += 1 dh.nextPage += 1
return nextPage return nextPage
@@ -44,6 +52,10 @@ func (dh *Dehasher) getNextPage() int {
func (dh *Dehasher) setQueries() { func (dh *Dehasher) setQueries() {
var numQueries int var numQueries int
if dh.debug {
debug.PrintInfo("setting queries")
}
switch { switch {
case dh.options.MaxRequests == 0: case dh.options.MaxRequests == 0:
zap.L().Error("max requests cannot be zero") zap.L().Error("max requests cannot be zero")
@@ -80,6 +92,12 @@ func (dh *Dehasher) setQueries() {
} }
dh.options.MaxRequests = numQueries dh.options.MaxRequests = numQueries
if dh.debug {
debug.PrintInfo(fmt.Sprintf("setting max requests: %d", numQueries))
debug.PrintInfo(fmt.Sprintf("setting max records: %d", dh.options.MaxRecords))
}
fmt.Printf("Making %d Requests for %d Records (%d Total)\n", dh.options.MaxRequests, dh.options.MaxRecords, dh.options.MaxRequests*dh.options.MaxRecords) fmt.Printf("Making %d Requests for %d Records (%d Total)\n", dh.options.MaxRequests, dh.options.MaxRecords, dh.options.MaxRequests*dh.options.MaxRecords)
} }
@@ -88,8 +106,13 @@ func (dh *Dehasher) Start() {
fmt.Printf("[*] Querying Dehashed API...\n") fmt.Printf("[*] Querying Dehashed API...\n")
for i := 0; i < dh.options.MaxRequests; i++ { for i := 0; i < dh.options.MaxRequests; i++ {
fmt.Printf(" [*] Performing Request...\n") fmt.Printf(" [*] Performing Request...\n")
count, err := dh.client.Search(*dh.request) count, balance, err := dh.client.Search(*dh.request)
if err != nil { if err != nil {
if dh.debug {
debug.PrintInfo("error performing request")
debug.PrintError(err)
}
// Check if it's a DehashError // Check if it's a DehashError
if dhErr, ok := err.(*DehashError); ok { if dhErr, ok := err.(*DehashError); ok {
fmt.Printf(" [!] Dehashed API Error: %s (Code: %d)\n", dhErr.Message, dhErr.Code) fmt.Printf(" [!] Dehashed API Error: %s (Code: %d)\n", dhErr.Message, dhErr.Code)
@@ -104,9 +127,24 @@ func (dh *Dehasher) Start() {
zap.Error(err), zap.Error(err),
) )
} }
if len(dh.client.results) > 0 {
fmt.Printf(" [!] Partial results retrieved. Storing Results...\n")
err := sqlite.StoreResults(dh.client.GetResults())
if err != nil {
zap.L().Error("store_results",
zap.String("message", "failed to store results"),
zap.Error(err),
)
fmt.Printf(" [!] Error storing results: %v\n", err)
}
}
dh.parseResults()
os.Exit(-1) os.Exit(-1)
} }
dh.balance = balance
if count < dh.options.MaxRecords { if count < dh.options.MaxRecords {
fmt.Printf(" [+] Retrieved %d records\n", count) fmt.Printf(" [+] Retrieved %d records\n", count)
fmt.Printf(" [-] Not enough entries, ending queries\n") fmt.Printf(" [-] Not enough entries, ending queries\n")
@@ -115,6 +153,10 @@ func (dh *Dehasher) Start() {
fmt.Printf(" [+] Retrieved %d records\n", dh.options.MaxRecords) fmt.Printf(" [+] Retrieved %d records\n", dh.options.MaxRecords)
} }
if dh.options.PrintBalance {
fmt.Printf(" [*] Balance: %d\n", balance)
}
dh.request.Page = dh.getNextPage() dh.request.Page = dh.getNextPage()
} }
@@ -1,4 +1,4 @@
package query package dehashed
type DehashError struct { type DehashError struct {
Message string Message string
+119
View File
@@ -11,6 +11,7 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"strings" "strings"
"time"
) )
func WriteCredsToFile(creds []sqlite.Creds, outputFile string, fileType files.FileType) error { func WriteCredsToFile(creds []sqlite.Creds, outputFile string, fileType files.FileType) error {
@@ -131,3 +132,121 @@ func WriteQueryResultsToFile(results []map[string]interface{}, outputFile string
filePath := fmt.Sprintf("%s.%s", outputFile, fileType.String()) filePath := fmt.Sprintf("%s.%s", outputFile, fileType.String())
return os.WriteFile(filePath, data, 0644) return os.WriteFile(filePath, data, 0644)
} }
func WriteWhoIsHistoryToFile(results []sqlite.HistoryRecord, outputFile string, fileType files.FileType) error {
var data []byte
var err error
switch fileType {
case files.JSON:
data, err = json.MarshalIndent(results, "", " ")
case files.XML:
data, err = xml.MarshalIndent(results, "", " ")
case files.YAML:
data, err = yaml.Marshal(results)
case files.TEXT:
var outStrings []string
for _, r := range results {
outStrings = append(outStrings, r.String()+"\n\n")
}
data = []byte(strings.Join(outStrings, ""))
default:
return errors.New("unsupported file type")
}
if err != nil {
return err
}
filePath := fmt.Sprintf("%s.%s", outputFile, fileType.String())
return os.WriteFile(filePath, data, 0644)
}
func WriteWhoIsRecordToFile(record sqlite.WhoisRecord, outputFile string, fileType files.FileType) error {
var data []byte
var err error
switch fileType {
case files.JSON:
data, err = json.MarshalIndent(record, "", " ")
case files.XML:
data, err = xml.MarshalIndent(record, "", " ")
case files.YAML:
data, err = yaml.Marshal(record)
case files.TEXT:
data = []byte(record.String())
default:
return errors.New("unsupported file type")
}
if err != nil {
return err
}
filePath := fmt.Sprintf("%s.%s", outputFile, fileType.String())
return os.WriteFile(filePath, data, 0644)
}
func WriteSubdomainsToFile(records []sqlite.SubdomainRecord, outputFile string, fileType files.FileType) error {
var data []byte
var err error
switch fileType {
case files.JSON:
data, err = json.MarshalIndent(records, "", " ")
case files.XML:
data, err = xml.MarshalIndent(records, "", " ")
case files.YAML:
data, err = yaml.Marshal(records)
case files.TEXT:
var outStrings []string
for _, r := range records {
out := fmt.Sprintf(
"Domain: %s\nFirst Seen: %s\nLast Seen: %s\n\n",
r.Domain, time.Unix(r.FirstSeen, 0).String(), time.Unix(r.LastSeen, 0).String())
outStrings = append(outStrings, out)
}
data = []byte(strings.Join(outStrings, ""))
default:
return errors.New("unsupported file type")
}
if err != nil {
return err
}
filePath := fmt.Sprintf("%s.%s", outputFile, fileType.String())
return os.WriteFile(filePath, data, 0644)
}
func WriteIPLookupToFile(records []sqlite.LookupResult, outputFile string, fileType files.FileType) error {
var data []byte
var err error
switch fileType {
case files.JSON:
data, err = json.MarshalIndent(records, "", " ")
case files.XML:
data, err = xml.MarshalIndent(records, "", " ")
case files.YAML:
data, err = yaml.Marshal(records)
case files.TEXT:
var outStrings []string
for _, r := range records {
out := fmt.Sprintf(
"Name: %s\nSearch Term: %s\nFirst Seen: %s\nLast Visit: %s\nType: %s\n\n",
r.Name, r.SearchTerm, time.Unix(r.FirstSeen, 0).String(), time.Unix(r.LastVisit, 0).String(), r.Type)
outStrings = append(outStrings, out)
}
data = []byte(strings.Join(outStrings, ""))
default:
return errors.New("unsupported file type")
}
if err != nil {
return err
}
filePath := fmt.Sprintf("%s.%s", outputFile, fileType.String())
return os.WriteFile(filePath, data, 0644)
}
+1
View File
@@ -7,6 +7,7 @@ const (
XML XML
YAML YAML
TEXT TEXT
UNKNOWN
) )
func GetFileType(filetype string) FileType { func GetFileType(filetype string) FileType {
+2 -2
View File
@@ -176,7 +176,7 @@ func QueryRuns(limit, lastXRuns int, startDate, endDate time.Time, containsQuery
// Apply query filter if provided // Apply query filter if provided
if containsQuery != "" { if containsQuery != "" {
// Search in all query fields // SearchTerm in all query fields
query = query.Where( query = query.Where(
"username_query LIKE ? OR "+ "username_query LIKE ? OR "+
"email_query LIKE ? OR "+ "email_query LIKE ? OR "+
@@ -238,7 +238,7 @@ func GetRunsCount(lastXRuns int, startDate, endDate time.Time, containsQuery str
// Apply query filter if provided // Apply query filter if provided
if containsQuery != "" { if containsQuery != "" {
// Search in all query fields // SearchTerm in all query fields
query = query.Where( query = query.Where(
"username_query LIKE ? OR "+ "username_query LIKE ? OR "+
"email_query LIKE ? OR "+ "email_query LIKE ? OR "+
+33 -2
View File
@@ -51,7 +51,7 @@ func InitDB(dbPath string) (*gorm.DB, error) {
} }
// Auto migrate your models // Auto migrate your models
err = db.AutoMigrate(&Result{}, &Creds{}, QueryOptions{}, Creds{}, WhoisRecord{}, SubdomainRecord{}, HistoryRecord{}) err = db.AutoMigrate(&Result{}, &Creds{}, &QueryOptions{}, &Creds{}, &WhoisRecord{}, &SubdomainRecord{}, &HistoryRecord{}, &LookupResult{})
if err != nil { if err != nil {
zap.L().Error("Failed to migrate database", zap.Error(err)) zap.L().Error("Failed to migrate database", zap.Error(err))
return nil, fmt.Errorf("failed to migrate database: %w", err) return nil, fmt.Errorf("failed to migrate database: %w", err)
@@ -163,7 +163,7 @@ func StoreWhoisRecord(whoisRecord WhoisRecord) error {
return nil return nil
} }
func StoreSubdomainRecord(subdomainRecords []SubdomainRecord) error { func StoreSubdomainRecords(subdomainRecords []SubdomainRecord) error {
if len(subdomainRecords) == 0 { if len(subdomainRecords) == 0 {
return nil return nil
} }
@@ -224,3 +224,34 @@ func StoreHistoryRecord(historyRecords []HistoryRecord) error {
return lastErr return lastErr
} }
func StoreIPLookup(ipLookup []LookupResult) error {
if len(ipLookup) == 0 {
return nil
}
zap.L().Info("Storing IP lookup records", zap.Int("count", len(ipLookup)))
db := GetDB()
// Use batch insert with conflict handling
const batchSize = 100
var lastErr error
for i := 0; i < len(ipLookup); i += batchSize {
end := i + batchSize
if end > len(ipLookup) {
end = len(ipLookup)
}
batch := ipLookup[i:end]
// Use Clauses with OnConflict DoNothing to skip conflicts
err := db.Clauses(clause.OnConflict{DoNothing: true}).CreateInBatches(&batch, batchSize).Error
if err != nil {
zap.L().Warn("Error storing some IP lookup records", zap.Error(err))
lastErr = err
// Continue with next batch despite error
}
}
return lastErr
}
+6 -4
View File
@@ -67,13 +67,14 @@ type QueryOptions struct {
CryptoAddressQuery string `json:"crypto_address_query"` CryptoAddressQuery string `json:"crypto_address_query"`
PrintBalance bool `json:"print_balance"` PrintBalance bool `json:"print_balance"`
CredsOnly bool `json:"creds_only"` CredsOnly bool `json:"creds_only"`
Debug bool `json:"debug"`
} }
func (QueryOptions) TableName() string { func (QueryOptions) TableName() string {
return "query_options" return "query_options"
} }
func NewQueryOptions(maxRecords, maxRequests, startingPage int, outputFormat, outputFile, usernameQuery, emailQuery, ipQuery, passQuery, hashQuery, nameQuery, domainQuery, vinQuery, licensePlateQuery, addressQuery, phoneQuery, socialQuery, cryptoAddressQuery string, regexMatch, wildcardMatch, printBalance, credsOnly bool) *QueryOptions { func NewQueryOptions(maxRecords, maxRequests, startingPage int, outputFormat, outputFile, usernameQuery, emailQuery, ipQuery, passQuery, hashQuery, nameQuery, domainQuery, vinQuery, licensePlateQuery, addressQuery, phoneQuery, socialQuery, cryptoAddressQuery string, regexMatch, wildcardMatch, printBalance, credsOnly, debug bool) *QueryOptions {
return &QueryOptions{ return &QueryOptions{
MaxRecords: maxRecords, MaxRecords: maxRecords,
MaxRequests: maxRequests, MaxRequests: maxRequests,
@@ -97,14 +98,15 @@ func NewQueryOptions(maxRecords, maxRequests, startingPage int, outputFormat, ou
PhoneQuery: phoneQuery, PhoneQuery: phoneQuery,
SocialQuery: socialQuery, SocialQuery: socialQuery,
CryptoAddressQuery: cryptoAddressQuery, CryptoAddressQuery: cryptoAddressQuery,
Debug: debug,
} }
} }
type Creds struct { type Creds struct {
gorm.Model gorm.Model
Email string `json:"email" yaml:"email" xml:"email"` Email string `json:"email" yaml:"email" xml:"email" gorm:"uniqueIndex:idx_email_username_password"`
Username string `json:"username" yaml:"username" xml:"username"` Username string `json:"username" yaml:"username" xml:"username" gorm:"uniqueIndex:idx_email_username_password"`
Password string `json:"password" yaml:"password" xml:"password"` Password string `json:"password" yaml:"password" xml:"password" gorm:"uniqueIndex:idx_email_username_password"`
} }
func (Creds) TableName() string { func (Creds) TableName() string {
+346 -4
View File
@@ -1,6 +1,10 @@
package sqlite package sqlite
import "gorm.io/gorm" import (
"fmt"
"gorm.io/gorm"
"strings"
)
type WhoIsLookupResult struct { type WhoIsLookupResult struct {
RemainingCredits int `json:"remaining_credits"` RemainingCredits int `json:"remaining_credits"`
@@ -17,7 +21,7 @@ type WhoisRecord struct {
ContactEmail string `json:"contactEmail"` ContactEmail string `json:"contactEmail"`
CreatedDate string `json:"createdDate"` CreatedDate string `json:"createdDate"`
CreatedDateNormalized string `json:"createdDateNormalized"` CreatedDateNormalized string `json:"createdDateNormalized"`
DomainName string `json:"domainName"` DomainName string `json:"domainName" gorm:"unique"`
DomainNameExt string `json:"domainNameExt"` DomainNameExt string `json:"domainNameExt"`
EstimatedDomainAge int `json:"estimatedDomainAge"` EstimatedDomainAge int `json:"estimatedDomainAge"`
ExpiresDate string `json:"expiresDate"` ExpiresDate string `json:"expiresDate"`
@@ -38,6 +42,142 @@ type WhoisRecord struct {
UpdatedDateNormalized string `json:"updatedDateNormalized"` UpdatedDateNormalized string `json:"updatedDateNormalized"`
} }
func (w WhoisRecord) String() string {
var sb strings.Builder
// Main domain information
sb.WriteString(fmt.Sprintf("Domain Name: %s\n", w.DomainName))
sb.WriteString(fmt.Sprintf("Domain Name Ext: %s\n", w.DomainNameExt))
sb.WriteString(fmt.Sprintf("Registrar Name: %s\n", w.RegistrarName))
sb.WriteString(fmt.Sprintf("Registrar IANA ID: %s\n", w.RegistrarIANAID))
sb.WriteString(fmt.Sprintf("Contact Email: %s\n", w.ContactEmail))
sb.WriteString(fmt.Sprintf("Estimated Domain Age: %d days\n", w.EstimatedDomainAge))
// Dates
sb.WriteString(fmt.Sprintf("Created Date: %s (Normalized: %s)\n", w.CreatedDate, w.CreatedDateNormalized))
sb.WriteString(fmt.Sprintf("Updated Date: %s (Normalized: %s)\n", w.UpdatedDate, w.UpdatedDateNormalized))
sb.WriteString(fmt.Sprintf("Expires Date: %s (Normalized: %s)\n", w.ExpiresDate, w.ExpiresDateNormalized))
// Status
sb.WriteString(fmt.Sprintf("Status: %s\n", w.Status))
// Parse code
sb.WriteString(fmt.Sprintf("Parse Code: %d\n", w.ParseCode))
// Audit information
sb.WriteString("\nAudit Information:\n")
sb.WriteString(fmt.Sprintf(" Created Date: %s\n", w.Audit.CreatedDate))
sb.WriteString(fmt.Sprintf(" Updated Date: %s\n", w.Audit.UpdatedDate))
// Name servers
sb.WriteString("\nName Servers:\n")
if len(w.NameServers.HostNames) > 0 {
for i, ns := range w.NameServers.HostNames {
ip := ""
if i < len(w.NameServers.IPs) {
ip = fmt.Sprintf(" (%s)", w.NameServers.IPs[i])
}
sb.WriteString(fmt.Sprintf(" %d. %s%s\n", i+1, ns, ip))
}
} else {
sb.WriteString(" None listed\n")
}
if w.NameServers.RawText != "" {
sb.WriteString(fmt.Sprintf(" Raw Text: %s\n", w.NameServers.RawText))
}
// Contact information
sb.WriteString("\nRegistrant Contact:\n")
formatWhoisContact(&sb, w.Registrant, " ")
sb.WriteString("\nTechnical Contact:\n")
formatWhoisContact(&sb, w.TechnicalContact, " ")
// Registry Data
sb.WriteString("\nRegistry Data:\n")
if w.RegistryData.DomainName != "" {
sb.WriteString(fmt.Sprintf(" Domain Name: %s\n", w.RegistryData.DomainName))
sb.WriteString(fmt.Sprintf(" Registrar Name: %s\n", w.RegistryData.RegistrarName))
sb.WriteString(fmt.Sprintf(" Registrar IANA ID: %s\n", w.RegistryData.RegistrarIANAID))
sb.WriteString(fmt.Sprintf(" Whois Server: %s\n", w.RegistryData.WhoisServer))
sb.WriteString(fmt.Sprintf(" Status: %s\n", w.RegistryData.Status))
// Registry dates
sb.WriteString(fmt.Sprintf(" Created Date: %s (Normalized: %s)\n",
w.RegistryData.CreatedDate, w.RegistryData.CreatedDateNormalized))
sb.WriteString(fmt.Sprintf(" Updated Date: %s (Normalized: %s)\n",
w.RegistryData.UpdatedDate, w.RegistryData.UpdatedDateNormalized))
sb.WriteString(fmt.Sprintf(" Expires Date: %s (Normalized: %s)\n",
w.RegistryData.ExpiresDate, w.RegistryData.ExpiresDateNormalized))
// Registry nameservers
sb.WriteString(" Name Servers:\n")
if len(w.RegistryData.NameServers.HostNames) > 0 {
for i, ns := range w.RegistryData.NameServers.HostNames {
ip := ""
if i < len(w.RegistryData.NameServers.IPs) {
ip = fmt.Sprintf(" (%s)", w.RegistryData.NameServers.IPs[i])
}
sb.WriteString(fmt.Sprintf(" %d. %s%s\n", i+1, ns, ip))
}
} else {
sb.WriteString(" None listed\n")
}
// Registry audit
sb.WriteString(" Audit Information:\n")
sb.WriteString(fmt.Sprintf(" Created Date: %s\n", w.RegistryData.Audit.CreatedDate))
sb.WriteString(fmt.Sprintf(" Updated Date: %s\n", w.RegistryData.Audit.UpdatedDate))
} else {
sb.WriteString(" No registry data available\n")
}
// Header and footer
if w.Header != "" {
headerPreview := w.Header
if len(headerPreview) > 100 {
headerPreview = headerPreview[:100] + "... [truncated]"
}
sb.WriteString("\nHeader:\n")
sb.WriteString(headerPreview)
sb.WriteString("\n")
}
if w.Footer != "" {
footerPreview := w.Footer
if len(footerPreview) > 100 {
footerPreview = footerPreview[:100] + "... [truncated]"
}
sb.WriteString("\nFooter:\n")
sb.WriteString(footerPreview)
sb.WriteString("\n")
}
// Raw text (truncated if too long)
if w.RawText != "" {
rawTextPreview := w.RawText
if len(rawTextPreview) > 500 {
rawTextPreview = rawTextPreview[:500] + "... [truncated]"
}
sb.WriteString("\nRaw Text:\n")
sb.WriteString(rawTextPreview)
sb.WriteString("\n")
}
if w.StrippedText != "" {
strippedTextPreview := w.StrippedText
if len(strippedTextPreview) > 500 {
strippedTextPreview = strippedTextPreview[:500] + "... [truncated]"
}
sb.WriteString("\nStripped Text:\n")
sb.WriteString(strippedTextPreview)
sb.WriteString("\n")
}
return sb.String()
}
func (WhoisRecord) TableName() string { func (WhoisRecord) TableName() string {
return "whois" return "whois"
} }
@@ -104,7 +244,7 @@ type ScanResult struct {
type SubdomainRecord struct { type SubdomainRecord struct {
gorm.Model gorm.Model
Domain string `json:"domain"` Domain string `json:"domain" gorm:"unique"`
FirstSeen int64 `json:"firstSeen"` FirstSeen int64 `json:"firstSeen"`
LastSeen int64 `json:"lastSeen"` LastSeen int64 `json:"lastSeen"`
} }
@@ -131,7 +271,7 @@ type HistoryRecord struct {
CleanText string `json:"cleanText"` CleanText string `json:"cleanText"`
CreatedDateISO8601 string `json:"createdDateISO8601"` CreatedDateISO8601 string `json:"createdDateISO8601"`
CreatedDateRaw string `json:"createdDateRaw"` CreatedDateRaw string `json:"createdDateRaw"`
DomainName string `json:"domainName"` DomainName string `json:"domainName" gorm:"unique"`
DomainType string `json:"domainType"` DomainType string `json:"domainType"`
ExpiresDateISO8601 string `json:"expiresDateISO8601"` ExpiresDateISO8601 string `json:"expiresDateISO8601"`
ExpiresDateRaw string `json:"expiresDateRaw"` ExpiresDateRaw string `json:"expiresDateRaw"`
@@ -147,6 +287,131 @@ type HistoryRecord struct {
ZoneContact ContactInfo `json:"zoneContact" gorm:"serializer:json"` ZoneContact ContactInfo `json:"zoneContact" gorm:"serializer:json"`
} }
func (h HistoryRecord) String() string {
var sb strings.Builder
// Main domain information
sb.WriteString(fmt.Sprintf("Domain Name: %s\n", h.DomainName))
sb.WriteString(fmt.Sprintf("Domain Type: %s\n", h.DomainType))
sb.WriteString(fmt.Sprintf("Registrar Name: %s\n", h.RegistrarName))
sb.WriteString(fmt.Sprintf("Whois Server: %s\n", h.WhoisServer))
// Dates
sb.WriteString(fmt.Sprintf("Created Date: %s (Raw: %s)\n", h.CreatedDateISO8601, h.CreatedDateRaw))
sb.WriteString(fmt.Sprintf("Updated Date: %s (Raw: %s)\n", h.UpdatedDateISO8601, h.UpdatedDateRaw))
sb.WriteString(fmt.Sprintf("Expires Date: %s (Raw: %s)\n", h.ExpiresDateISO8601, h.ExpiresDateRaw))
// Status
sb.WriteString("Status: ")
if len(h.Status) > 0 {
sb.WriteString(strings.Join(h.Status, ", "))
} else {
sb.WriteString("N/A")
}
sb.WriteString("\n")
// Audit information
sb.WriteString("\nAudit Information:\n")
sb.WriteString(fmt.Sprintf(" Created Date: %s\n", h.Audit.CreatedDate))
sb.WriteString(fmt.Sprintf(" Updated Date: %s\n", h.Audit.UpdatedDate))
// Name servers
sb.WriteString("\nName Servers:\n")
if len(h.NameServers) > 0 {
for i, ns := range h.NameServers {
sb.WriteString(fmt.Sprintf(" %d. %s\n", i+1, ns))
}
} else {
sb.WriteString(" None listed\n")
}
// Contact information
sb.WriteString("\nRegistrant Contact:\n")
formatContact(&sb, h.RegistrantContact, " ")
sb.WriteString("\nAdministrative Contact:\n")
formatContact(&sb, h.AdministrativeContact, " ")
sb.WriteString("\nTechnical Contact:\n")
formatContact(&sb, h.TechnicalContact, " ")
sb.WriteString("\nBilling Contact:\n")
formatContact(&sb, h.BillingContact, " ")
sb.WriteString("\nZone Contact:\n")
formatContact(&sb, h.ZoneContact, " ")
// Raw text (truncated if too long)
if len(h.RawText) > 0 {
rawTextPreview := h.RawText
if len(rawTextPreview) > 500 {
rawTextPreview = rawTextPreview[:500] + "... [truncated]"
}
sb.WriteString("\nRaw Text:\n")
sb.WriteString(rawTextPreview)
sb.WriteString("\n")
}
if len(h.CleanText) > 0 {
cleanTextPreview := h.CleanText
if len(cleanTextPreview) > 500 {
cleanTextPreview = cleanTextPreview[:500] + "... [truncated]"
}
sb.WriteString("\nClean Text:\n")
sb.WriteString(cleanTextPreview)
sb.WriteString("\n")
}
return sb.String()
}
// Helper function to format contact information
func formatContact(sb *strings.Builder, contact ContactInfo, indent string) {
if contact.Name == "" && contact.Organization == "" && contact.Email == "" {
sb.WriteString(indent + "No contact information available\n")
return
}
if contact.Name != "" {
sb.WriteString(indent + "Name: " + contact.Name + "\n")
}
if contact.Organization != "" {
sb.WriteString(indent + "Organization: " + contact.Organization + "\n")
}
if contact.Email != "" {
sb.WriteString(indent + "Email: " + contact.Email + "\n")
}
if contact.Street != "" {
sb.WriteString(indent + "Street: " + contact.Street + "\n")
}
if contact.City != "" {
sb.WriteString(indent + "City: " + contact.City + "\n")
}
if contact.State != "" {
sb.WriteString(indent + "State: " + contact.State + "\n")
}
if contact.PostalCode != "" {
sb.WriteString(indent + "Postal Code: " + contact.PostalCode + "\n")
}
if contact.Country != "" {
sb.WriteString(indent + "Country: " + contact.Country + "\n")
}
if contact.Telephone != "" {
phone := contact.Telephone
if contact.TelephoneExt != "" {
phone += " ext. " + contact.TelephoneExt
}
sb.WriteString(indent + "Telephone: " + phone + "\n")
}
if contact.Fax != "" {
fax := contact.Fax
if contact.FaxExt != "" {
fax += " ext. " + contact.FaxExt
}
sb.WriteString(indent + "Fax: " + fax + "\n")
}
}
func (HistoryRecord) TableName() string { func (HistoryRecord) TableName() string {
return "history" return "history"
} }
@@ -170,3 +435,80 @@ type ContactInfo struct {
type WhoIsCredits struct { type WhoIsCredits struct {
WhoisCredits int `json:"whois_credits"` WhoisCredits int `json:"whois_credits"`
} }
type WhoIsIPLookup struct {
RemainingCredits int `json:"remaining_credits"`
Data IPData `json:"data"`
}
type WhoIsMXLookup struct {
RemainingCredits int `json:"remaining_credits"`
Data IPData `json:"data"`
}
type WhoIsNSLookup struct {
RemainingCredits int `json:"remaining_credits"`
Data IPData `json:"data"`
}
type IPData struct {
CurrentPage string `json:"current_page"`
Result []LookupResult `json:"result"`
Size int `json:"size"`
}
type LookupResult struct {
gorm.Model
FirstSeen int64 `json:"first_seen"`
LastVisit int64 `json:"last_visit"`
Name string `json:"name" gorm:"unique"`
SearchTerm string `json:"search_term,omitempty"` // For storing the IP address this domain is associated with
Type string `json:"type,omitempty"` // For storing the MX address this domain is associated with
}
func (LookupResult) TableName() string {
return "lookup"
}
// Helper function to format contact information for WhoisRecord
func formatWhoisContact(sb *strings.Builder, contact Contact, indent string) {
if contact.Name == "" && contact.Organization == "" {
sb.WriteString(indent + "No contact information available\n")
return
}
if contact.Name != "" {
sb.WriteString(indent + "Name: " + contact.Name + "\n")
}
if contact.Organization != "" {
sb.WriteString(indent + "Organization: " + contact.Organization + "\n")
}
if contact.Street1 != "" {
sb.WriteString(indent + "Street: " + contact.Street1 + "\n")
}
if contact.City != "" {
sb.WriteString(indent + "City: " + contact.City + "\n")
}
if contact.State != "" {
sb.WriteString(indent + "State: " + contact.State + "\n")
}
if contact.PostalCode != "" {
sb.WriteString(indent + "Postal Code: " + contact.PostalCode + "\n")
}
if contact.Country != "" {
sb.WriteString(indent + "Country: " + contact.Country + "\n")
}
if contact.CountryCode != "" {
sb.WriteString(indent + "Country Code: " + contact.CountryCode + "\n")
}
if contact.Telephone != "" {
sb.WriteString(indent + "Telephone: " + contact.Telephone + "\n")
}
if contact.RawText != "" {
rawTextPreview := contact.RawText
if len(rawTextPreview) > 100 {
rawTextPreview = rawTextPreview[:100] + "... [truncated]"
}
sb.WriteString(indent + "Raw Text: " + rawTextPreview + "\n")
}
}
+730 -139
View File
File diff suppressed because it is too large Load Diff