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
+302
View File
@@ -0,0 +1,302 @@
package dehashed
import (
"bytes"
"crypto/sha256"
"dehasher/internal/debug"
"dehasher/internal/sqlite"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"go.uber.org/zap"
"io"
"net/http"
"strings"
)
type DehashedParameter string
const (
Username DehashedParameter = "username"
Email DehashedParameter = "email"
Password DehashedParameter = "password"
HashedPassword DehashedParameter = "hashed_password"
Name DehashedParameter = "name"
IpAddress DehashedParameter = "ip_address"
Domain DehashedParameter = "domain"
Vin DehashedParameter = "vin"
LicensePlate DehashedParameter = "license_plate"
Address DehashedParameter = "address"
Phone DehashedParameter = "phone"
Social DehashedParameter = "social"
CryptoAddress DehashedParameter = "cryptocurrency_address"
)
func (dp DehashedParameter) GetArgumentString(arg string) string {
return fmt.Sprintf("%s:%s", string(dp), arg)
}
type DehashedSearchRequest struct {
ForcePlaintext bool `json:"-"`
Debug bool `json:"-"`
Page int `json:"page"`
Query string `json:"query"`
Size int `json:"size"`
Wildcard bool `json:"wildcard"`
Regex bool `json:"regex"`
DeDupe bool `json:"de_dupe"`
}
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, Debug: debug}
}
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 {
dsr.Query = fmt.Sprintf("%s&%s", strings.TrimSpace(dsr.Query), query)
} else {
dsr.Query = query
}
if dsr.Debug {
debug.PrintInfo(fmt.Sprintf("query built: %s", dsr.Query))
}
}
func (dsr *DehashedSearchRequest) AddUsernameQuery(query string) {
query = enquoteSpaced(query)
dsr.buildQuery(Username.GetArgumentString(query))
}
func (dsr *DehashedSearchRequest) AddEmailQuery(query string) {
query = enquoteSpaced(query)
dsr.buildQuery(Email.GetArgumentString(query))
}
func (dsr *DehashedSearchRequest) AddIpAddressQuery(query string) {
query = enquoteSpaced(query)
dsr.buildQuery(IpAddress.GetArgumentString(query))
}
func (dsr *DehashedSearchRequest) AddDomainQuery(query string) {
query = enquoteSpaced(query)
dsr.buildQuery(Domain.GetArgumentString(query))
}
func (dsr *DehashedSearchRequest) AddPasswordQuery(query string) {
if dsr.ForcePlaintext {
query = enquoteSpaced(query)
dsr.buildQuery(Password.GetArgumentString(query))
return
}
hash := sha256.Sum256([]byte(query))
query = hex.EncodeToString(hash[:])
dsr.AddHashedPasswordQuery(query)
}
func (dsr *DehashedSearchRequest) AddVinQuery(query string) {
query = enquoteSpaced(query)
dsr.buildQuery(Vin.GetArgumentString(query))
}
func (dsr *DehashedSearchRequest) AddLicensePlateQuery(query string) {
query = enquoteSpaced(query)
dsr.buildQuery(LicensePlate.GetArgumentString(query))
}
func (dsr *DehashedSearchRequest) AddAddressQuery(query string) {
query = enquoteSpaced(query)
dsr.buildQuery(Address.GetArgumentString(query))
}
func (dsr *DehashedSearchRequest) AddPhoneQuery(query string) {
query = enquoteSpaced(query)
dsr.buildQuery(Phone.GetArgumentString(query))
}
func (dsr *DehashedSearchRequest) AddSocialQuery(query string) {
query = enquoteSpaced(query)
dsr.buildQuery(Social.GetArgumentString(query))
}
func (dsr *DehashedSearchRequest) AddCryptoAddressQuery(query string) {
query = enquoteSpaced(query)
dsr.buildQuery(CryptoAddress.GetArgumentString(query))
}
func (dsr *DehashedSearchRequest) AddHashedPasswordQuery(query string) {
query = strings.TrimSpace(query)
dsr.buildQuery(HashedPassword.GetArgumentString(query))
}
func (dsr *DehashedSearchRequest) AddNameQuery(query string) {
query = enquoteSpaced(query)
dsr.buildQuery(Name.GetArgumentString(query))
}
type DehashedClientV2 struct {
apiKey string
results []sqlite.Result
debug bool
}
func NewDehashedClientV2(apiKey string, debug bool) *DehashedClientV2 {
return &DehashedClientV2{apiKey: apiKey, debug: debug}
}
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)
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))
if err != nil {
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("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)
if res != nil {
defer res.Body.Close()
}
if err != nil {
if dcv2.debug {
debug.PrintInfo("failed to perform request")
debug.PrintError(err)
}
zap.L().Error("v2_search",
zap.String("message", "failed to perform request"),
zap.Error(err),
)
return -1, -1, err
}
if res == nil {
if dcv2.debug {
debug.PrintInfo("response was nil")
}
zap.L().Error("v2_search",
zap.String("message", "response was nil"),
)
return -1, -1, errors.New("response was nil")
}
b, err := io.ReadAll(res.Body)
if err != nil {
zap.L().Error("v2_search",
zap.String("message", "failed to read response body"),
zap.Error(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
err = json.Unmarshal(b, &responseResults)
if err != nil {
if dcv2.debug {
debug.PrintInfo("failed to unmarshal response body")
debug.PrintError(err)
}
zap.L().Error("v2_search",
zap.String("message", "failed to unmarshal response body"),
zap.Error(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...)
return responseResults.TotalResults, responseResults.Balance, nil
}
func (dcv2 *DehashedClientV2) GetResults() sqlite.DehashedResults {
return sqlite.DehashedResults{Results: dcv2.results}
}
func (dcv2 *DehashedClientV2) GetTotalResults() int {
return len(dcv2.results)
}
func enquoteSpaced(s string) string {
s = strings.TrimSpace(s)
if strings.Contains(s, " ") {
return fmt.Sprintf("\"%s\"", s)
}
return s
}
+261
View File
@@ -0,0 +1,261 @@
package dehashed
import (
"dehasher/internal/debug"
"dehasher/internal/export"
"dehasher/internal/sqlite"
"encoding/json"
"fmt"
"go.uber.org/zap"
"os"
)
// Dehasher is a struct for querying the Dehashed API
type Dehasher struct {
options sqlite.QueryOptions
nextPage int
debug bool
balance int
request *DehashedSearchRequest
client *DehashedClientV2
}
// NewDehasher creates a new Dehasher
func NewDehasher(options *sqlite.QueryOptions) *Dehasher {
dh := &Dehasher{
options: *options,
nextPage: options.StartingPage + 1,
debug: options.Debug,
balance: 0,
}
dh.setQueries()
dh.request = NewDehashedSearchRequest(dh.options.StartingPage, dh.options.MaxRecords, dh.options.WildcardMatch, dh.options.RegexMatch, false, options.Debug)
dh.buildRequest()
return dh
}
// SetClientCredentials sets the client credentials for the dehasher
func (dh *Dehasher) SetClientCredentials(key string) {
dh.client = NewDehashedClientV2(key, dh.debug)
}
func (dh *Dehasher) getNextPage() int {
if dh.debug {
debug.PrintInfo(fmt.Sprintf("getting next page: %d", dh.nextPage))
}
nextPage := dh.nextPage
dh.nextPage += 1
return nextPage
}
// setQueries sets the number of queries to make based on the number of records and requests
func (dh *Dehasher) setQueries() {
var numQueries int
if dh.debug {
debug.PrintInfo("setting queries")
}
switch {
case dh.options.MaxRequests == 0:
zap.L().Error("max requests cannot be zero")
fmt.Println("[!] Max Requests cannot be zero")
os.Exit(1)
case dh.options.MaxRecords <= 10000 || dh.options.MaxRequests == 1:
numQueries = 1
if dh.options.MaxRecords > 10000 {
dh.options.MaxRecords = 10000
}
zap.L().Info("max requests set to 1", zap.Int("max_records", dh.options.MaxRecords))
case dh.options.MaxRequests < 0 && dh.options.MaxRecords > 20000:
numQueries = 3
dh.options.MaxRecords = 10000
zap.L().Info("max requests set to 3", zap.Int("max_records", dh.options.MaxRecords))
case dh.options.MaxRequests < 0 && dh.options.MaxRecords > 10000:
numQueries = 2
dh.options.MaxRecords = 10000
zap.L().Info("max requests set to 2", zap.Int("max_records", dh.options.MaxRecords))
case dh.options.MaxRecords < 0 && dh.options.MaxRecords < 10000:
numQueries = 1
zap.L().Info("max requests set to 1", zap.Int("max_records", dh.options.MaxRecords))
case dh.options.MaxRequests == 2 && dh.options.MaxRecords > 20000:
numQueries = 2
dh.options.MaxRecords = 10000
zap.L().Info("max requests set to 2", zap.Int("max_records", dh.options.MaxRecords))
case dh.options.MaxRequests == 2 && dh.options.MaxRecords <= 10000:
numQueries = 1
zap.L().Info("max requests set to 1", zap.Int("max_records", dh.options.MaxRecords))
default:
numQueries = 3
dh.options.MaxRecords = 10000
zap.L().Info("max requests set to 3", zap.Int("max_records", dh.options.MaxRecords))
}
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)
}
// Start starts the querying process
func (dh *Dehasher) Start() {
fmt.Printf("[*] Querying Dehashed API...\n")
for i := 0; i < dh.options.MaxRequests; i++ {
fmt.Printf(" [*] Performing Request...\n")
count, balance, err := dh.client.Search(*dh.request)
if err != nil {
if dh.debug {
debug.PrintInfo("error performing request")
debug.PrintError(err)
}
// Check if it's a DehashError
if dhErr, ok := err.(*DehashError); ok {
fmt.Printf(" [!] Dehashed API Error: %s (Code: %d)\n", dhErr.Message, dhErr.Code)
zap.L().Error("dehashed_api_error",
zap.String("message", dhErr.Message),
zap.Int("code", dhErr.Code),
)
} else {
fmt.Printf(" [!] Error performing request: %v\n", err)
zap.L().Error("request_error",
zap.String("message", "failed to perform request"),
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)
}
dh.balance = balance
if count < dh.options.MaxRecords {
fmt.Printf(" [+] Retrieved %d records\n", count)
fmt.Printf(" [-] Not enough entries, ending queries\n")
break
} else {
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.parseResults()
}
// buildRequest constructs the query map
func (dh *Dehasher) buildRequest() {
if len(dh.options.UsernameQuery) > 0 {
dh.request.AddUsernameQuery(dh.options.UsernameQuery)
}
if len(dh.options.EmailQuery) > 0 {
dh.request.AddEmailQuery(dh.options.EmailQuery)
}
if len(dh.options.IpQuery) > 0 {
dh.request.AddIpAddressQuery(dh.options.IpQuery)
}
if len(dh.options.HashQuery) > 0 {
dh.request.AddHashedPasswordQuery(dh.options.HashQuery)
}
if len(dh.options.PassQuery) > 0 {
dh.request.AddPasswordQuery(dh.options.PassQuery)
}
if len(dh.options.NameQuery) > 0 {
dh.request.AddNameQuery(dh.options.NameQuery)
}
if len(dh.options.DomainQuery) > 0 {
dh.request.AddDomainQuery(dh.options.DomainQuery)
}
if len(dh.options.VinQuery) > 0 {
dh.request.AddVinQuery(dh.options.VinQuery)
}
if len(dh.options.LicensePlateQuery) > 0 {
dh.request.AddLicensePlateQuery(dh.options.LicensePlateQuery)
}
if len(dh.options.AddressQuery) > 0 {
dh.request.AddAddressQuery(dh.options.AddressQuery)
}
if len(dh.options.PhoneQuery) > 0 {
dh.request.AddPhoneQuery(dh.options.PhoneQuery)
}
if len(dh.options.SocialQuery) > 0 {
dh.request.AddSocialQuery(dh.options.SocialQuery)
}
if len(dh.options.CryptoAddressQuery) > 0 {
dh.request.AddCryptoAddressQuery(dh.options.CryptoAddressQuery)
}
}
// parseResults parses the results and writes them to a file
func (dh *Dehasher) parseResults() {
var data []byte
zap.L().Info("extracting_credentials")
results := dh.client.GetResults()
creds := results.ExtractCredentials()
fmt.Printf("\n\t[+] Discovered %d Credentials", len(creds))
err := sqlite.StoreCreds(creds)
if err != nil {
zap.L().Error("store_creds",
zap.String("message", "failed to store creds"),
zap.Error(err),
)
}
zap.L().Info("creds_stored", zap.Int("count", len(creds)))
zap.L().Info("storing_results")
err = sqlite.StoreResults(results)
if err != nil {
zap.L().Error("store_results",
zap.String("message", "failed to store results"),
zap.Error(err),
)
}
zap.L().Info("results_stored", zap.Int("count", len(results.Results)))
if len(results.Results) > 0 {
fmt.Printf("\n\t[*] Writing entries to file: %s.%s", dh.options.OutputFile, dh.options.OutputFormat.String())
if !dh.options.CredsOnly {
err := export.WriteToFile(results, dh.options.OutputFile, dh.options.OutputFormat)
if err != nil {
fmt.Printf("\n[!] Error Writing to file: %v\n\tOutputting to terminal.", err)
data, err = json.MarshalIndent(results, "", " ")
fmt.Println(string(data))
os.Exit(0)
} else {
fmt.Println("\n\t\t[*] Success\n")
}
} else {
creds := results.ExtractCredentials()
err := export.WriteCredsToFile(creds, dh.options.OutputFile, dh.options.OutputFormat)
if err != nil {
fmt.Printf("\n[!] Error Writing to file: %v\n\tOutputting to terminal.", err)
data, err = json.MarshalIndent(creds, "", " ")
fmt.Println(string(data))
os.Exit(0)
} else {
fmt.Println("\n\t\t[*] Success\n")
}
}
}
}
+33
View File
@@ -0,0 +1,33 @@
package dehashed
type DehashError struct {
Message string
Code int
}
type DehashResponseError struct {
HttpResponse int `json:"HTTP Response Code"`
}
func (de *DehashError) Error() string {
return de.Message
}
func GetDehashedError(c int) DehashError {
switch c {
case 400:
return DehashError{Code: 400, Message: "There is an issue with authentication. Please check your API key and email. If you haven't, refresh your API Key "}
case 401:
return DehashError{Code: 401, Message: "You need a search subscription and API credits to use the API, please purchase a search subscription and add credits to your account."}
case 403:
return DehashError{Code: 403, Message: "Insufficient Credits"}
case 404:
return DehashError{Code: 404, Message: "Method not permitted"}
case 429:
return DehashError{Code: 420, Message: "Rate Limited"}
case 302:
return DehashError{Code: 302, Message: "Invalid/Missing Query"}
default:
return DehashError{Code: -1, Message: "An unknown error has occurred"}
}
}