diff --git a/dns_tools.go b/dns_tools.go new file mode 100644 index 0000000..f1b31fa --- /dev/null +++ b/dns_tools.go @@ -0,0 +1,850 @@ +package poindexter + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/url" + "sort" + "strings" + "time" +) + +// ============================================================================ +// DNS Record Types +// ============================================================================ + +// DNSRecordType represents DNS record types +type DNSRecordType string + +const ( + DNSRecordA DNSRecordType = "A" + DNSRecordAAAA DNSRecordType = "AAAA" + DNSRecordMX DNSRecordType = "MX" + DNSRecordTXT DNSRecordType = "TXT" + DNSRecordNS DNSRecordType = "NS" + DNSRecordCNAME DNSRecordType = "CNAME" + DNSRecordSOA DNSRecordType = "SOA" + DNSRecordPTR DNSRecordType = "PTR" + DNSRecordSRV DNSRecordType = "SRV" + DNSRecordCAA DNSRecordType = "CAA" +) + +// DNSRecord represents a generic DNS record +type DNSRecord struct { + Type DNSRecordType `json:"type"` + Name string `json:"name"` + Value string `json:"value"` + TTL int `json:"ttl,omitempty"` +} + +// MXRecord represents an MX record with priority +type MXRecord struct { + Host string `json:"host"` + Priority uint16 `json:"priority"` +} + +// SRVRecord represents an SRV record +type SRVRecord struct { + Target string `json:"target"` + Port uint16 `json:"port"` + Priority uint16 `json:"priority"` + Weight uint16 `json:"weight"` +} + +// SOARecord represents an SOA record +type SOARecord struct { + PrimaryNS string `json:"primaryNs"` + AdminEmail string `json:"adminEmail"` + Serial uint32 `json:"serial"` + Refresh uint32 `json:"refresh"` + Retry uint32 `json:"retry"` + Expire uint32 `json:"expire"` + MinTTL uint32 `json:"minTtl"` +} + +// DNSLookupResult contains the results of a DNS lookup +type DNSLookupResult struct { + Domain string `json:"domain"` + QueryType string `json:"queryType"` + Records []DNSRecord `json:"records"` + MXRecords []MXRecord `json:"mxRecords,omitempty"` + SRVRecords []SRVRecord `json:"srvRecords,omitempty"` + SOARecord *SOARecord `json:"soaRecord,omitempty"` + LookupTimeMs int64 `json:"lookupTimeMs"` + Error string `json:"error,omitempty"` + Timestamp time.Time `json:"timestamp"` +} + +// CompleteDNSLookup contains all DNS records for a domain +type CompleteDNSLookup struct { + Domain string `json:"domain"` + A []string `json:"a,omitempty"` + AAAA []string `json:"aaaa,omitempty"` + MX []MXRecord `json:"mx,omitempty"` + NS []string `json:"ns,omitempty"` + TXT []string `json:"txt,omitempty"` + CNAME string `json:"cname,omitempty"` + SOA *SOARecord `json:"soa,omitempty"` + LookupTimeMs int64 `json:"lookupTimeMs"` + Errors []string `json:"errors,omitempty"` + Timestamp time.Time `json:"timestamp"` +} + +// ============================================================================ +// DNS Lookup Functions +// ============================================================================ + +// DNSLookup performs a DNS lookup for the specified record type +func DNSLookup(domain string, recordType DNSRecordType) DNSLookupResult { + return DNSLookupWithTimeout(domain, recordType, 10*time.Second) +} + +// DNSLookupWithTimeout performs a DNS lookup with a custom timeout +func DNSLookupWithTimeout(domain string, recordType DNSRecordType, timeout time.Duration) DNSLookupResult { + start := time.Now() + result := DNSLookupResult{ + Domain: domain, + QueryType: string(recordType), + Timestamp: start, + } + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + resolver := net.Resolver{} + + switch recordType { + case DNSRecordA: + ips, err := resolver.LookupIP(ctx, "ip4", domain) + if err != nil { + result.Error = err.Error() + } else { + for _, ip := range ips { + result.Records = append(result.Records, DNSRecord{ + Type: DNSRecordA, + Name: domain, + Value: ip.String(), + }) + } + } + + case DNSRecordAAAA: + ips, err := resolver.LookupIP(ctx, "ip6", domain) + if err != nil { + result.Error = err.Error() + } else { + for _, ip := range ips { + result.Records = append(result.Records, DNSRecord{ + Type: DNSRecordAAAA, + Name: domain, + Value: ip.String(), + }) + } + } + + case DNSRecordMX: + mxs, err := resolver.LookupMX(ctx, domain) + if err != nil { + result.Error = err.Error() + } else { + for _, mx := range mxs { + result.MXRecords = append(result.MXRecords, MXRecord{ + Host: strings.TrimSuffix(mx.Host, "."), + Priority: mx.Pref, + }) + result.Records = append(result.Records, DNSRecord{ + Type: DNSRecordMX, + Name: domain, + Value: fmt.Sprintf("%d %s", mx.Pref, mx.Host), + }) + } + // Sort by priority + sort.Slice(result.MXRecords, func(i, j int) bool { + return result.MXRecords[i].Priority < result.MXRecords[j].Priority + }) + } + + case DNSRecordTXT: + txts, err := resolver.LookupTXT(ctx, domain) + if err != nil { + result.Error = err.Error() + } else { + for _, txt := range txts { + result.Records = append(result.Records, DNSRecord{ + Type: DNSRecordTXT, + Name: domain, + Value: txt, + }) + } + } + + case DNSRecordNS: + nss, err := resolver.LookupNS(ctx, domain) + if err != nil { + result.Error = err.Error() + } else { + for _, ns := range nss { + result.Records = append(result.Records, DNSRecord{ + Type: DNSRecordNS, + Name: domain, + Value: strings.TrimSuffix(ns.Host, "."), + }) + } + } + + case DNSRecordCNAME: + cname, err := resolver.LookupCNAME(ctx, domain) + if err != nil { + result.Error = err.Error() + } else { + result.Records = append(result.Records, DNSRecord{ + Type: DNSRecordCNAME, + Name: domain, + Value: strings.TrimSuffix(cname, "."), + }) + } + + case DNSRecordSRV: + // SRV records require a service and protocol prefix, e.g., _http._tcp.example.com + _, srvs, err := resolver.LookupSRV(ctx, "", "", domain) + if err != nil { + result.Error = err.Error() + } else { + for _, srv := range srvs { + result.SRVRecords = append(result.SRVRecords, SRVRecord{ + Target: strings.TrimSuffix(srv.Target, "."), + Port: srv.Port, + Priority: srv.Priority, + Weight: srv.Weight, + }) + result.Records = append(result.Records, DNSRecord{ + Type: DNSRecordSRV, + Name: domain, + Value: fmt.Sprintf("%d %d %d %s", srv.Priority, srv.Weight, srv.Port, srv.Target), + }) + } + } + + case DNSRecordPTR: + names, err := resolver.LookupAddr(ctx, domain) + if err != nil { + result.Error = err.Error() + } else { + for _, name := range names { + result.Records = append(result.Records, DNSRecord{ + Type: DNSRecordPTR, + Name: domain, + Value: strings.TrimSuffix(name, "."), + }) + } + } + + default: + result.Error = fmt.Sprintf("unsupported record type: %s", recordType) + } + + result.LookupTimeMs = time.Since(start).Milliseconds() + return result +} + +// DNSLookupAll performs lookups for all common record types +func DNSLookupAll(domain string) CompleteDNSLookup { + return DNSLookupAllWithTimeout(domain, 10*time.Second) +} + +// DNSLookupAllWithTimeout performs lookups for all common record types with timeout +func DNSLookupAllWithTimeout(domain string, timeout time.Duration) CompleteDNSLookup { + start := time.Now() + result := CompleteDNSLookup{ + Domain: domain, + Timestamp: start, + } + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + resolver := net.Resolver{} + + // A records + if ips, err := resolver.LookupIP(ctx, "ip4", domain); err == nil { + for _, ip := range ips { + result.A = append(result.A, ip.String()) + } + } else if !isNoSuchHostError(err) { + result.Errors = append(result.Errors, fmt.Sprintf("A: %s", err.Error())) + } + + // AAAA records + if ips, err := resolver.LookupIP(ctx, "ip6", domain); err == nil { + for _, ip := range ips { + result.AAAA = append(result.AAAA, ip.String()) + } + } else if !isNoSuchHostError(err) { + result.Errors = append(result.Errors, fmt.Sprintf("AAAA: %s", err.Error())) + } + + // MX records + if mxs, err := resolver.LookupMX(ctx, domain); err == nil { + for _, mx := range mxs { + result.MX = append(result.MX, MXRecord{ + Host: strings.TrimSuffix(mx.Host, "."), + Priority: mx.Pref, + }) + } + sort.Slice(result.MX, func(i, j int) bool { + return result.MX[i].Priority < result.MX[j].Priority + }) + } else if !isNoSuchHostError(err) { + result.Errors = append(result.Errors, fmt.Sprintf("MX: %s", err.Error())) + } + + // NS records + if nss, err := resolver.LookupNS(ctx, domain); err == nil { + for _, ns := range nss { + result.NS = append(result.NS, strings.TrimSuffix(ns.Host, ".")) + } + } else if !isNoSuchHostError(err) { + result.Errors = append(result.Errors, fmt.Sprintf("NS: %s", err.Error())) + } + + // TXT records + if txts, err := resolver.LookupTXT(ctx, domain); err == nil { + result.TXT = txts + } else if !isNoSuchHostError(err) { + result.Errors = append(result.Errors, fmt.Sprintf("TXT: %s", err.Error())) + } + + // CNAME record + if cname, err := resolver.LookupCNAME(ctx, domain); err == nil { + result.CNAME = strings.TrimSuffix(cname, ".") + // If CNAME equals domain, it's not really a CNAME + if result.CNAME == domain { + result.CNAME = "" + } + } + + result.LookupTimeMs = time.Since(start).Milliseconds() + return result +} + +// ReverseDNSLookup performs a reverse DNS lookup for an IP address +func ReverseDNSLookup(ip string) DNSLookupResult { + return DNSLookupWithTimeout(ip, DNSRecordPTR, 10*time.Second) +} + +func isNoSuchHostError(err error) bool { + if err == nil { + return false + } + return strings.Contains(err.Error(), "no such host") || + strings.Contains(err.Error(), "NXDOMAIN") || + strings.Contains(err.Error(), "not found") +} + +// ============================================================================ +// RDAP (Registration Data Access Protocol) - New Style WHOIS +// ============================================================================ + +// RDAPResponse represents an RDAP response +type RDAPResponse struct { + // Common fields + Handle string `json:"handle,omitempty"` + LDHName string `json:"ldhName,omitempty"` // Domain name + UnicodeName string `json:"unicodeName,omitempty"` + Status []string `json:"status,omitempty"` + Events []RDAPEvent `json:"events,omitempty"` + Entities []RDAPEntity `json:"entities,omitempty"` + Nameservers []RDAPNs `json:"nameservers,omitempty"` + Links []RDAPLink `json:"links,omitempty"` + Remarks []RDAPRemark `json:"remarks,omitempty"` + Notices []RDAPNotice `json:"notices,omitempty"` + + // Network-specific (for IP lookups) + StartAddress string `json:"startAddress,omitempty"` + EndAddress string `json:"endAddress,omitempty"` + IPVersion string `json:"ipVersion,omitempty"` + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + Country string `json:"country,omitempty"` + ParentHandle string `json:"parentHandle,omitempty"` + + // Error fields + ErrorCode int `json:"errorCode,omitempty"` + Title string `json:"title,omitempty"` + Description []string `json:"description,omitempty"` + + // Metadata + RawJSON string `json:"rawJson,omitempty"` + LookupTimeMs int64 `json:"lookupTimeMs"` + Timestamp time.Time `json:"timestamp"` + Error string `json:"error,omitempty"` +} + +// RDAPEvent represents an RDAP event (registration, expiration, etc.) +type RDAPEvent struct { + EventAction string `json:"eventAction"` + EventDate string `json:"eventDate"` + EventActor string `json:"eventActor,omitempty"` +} + +// RDAPEntity represents an entity (registrar, registrant, etc.) +type RDAPEntity struct { + Handle string `json:"handle,omitempty"` + Roles []string `json:"roles,omitempty"` + VCardArray []any `json:"vcardArray,omitempty"` + Entities []RDAPEntity `json:"entities,omitempty"` + Events []RDAPEvent `json:"events,omitempty"` + Links []RDAPLink `json:"links,omitempty"` + Remarks []RDAPRemark `json:"remarks,omitempty"` +} + +// RDAPNs represents a nameserver in RDAP +type RDAPNs struct { + LDHName string `json:"ldhName"` + IPAddresses *RDAPIPs `json:"ipAddresses,omitempty"` +} + +// RDAPIPs represents IP addresses for a nameserver +type RDAPIPs struct { + V4 []string `json:"v4,omitempty"` + V6 []string `json:"v6,omitempty"` +} + +// RDAPLink represents a link in RDAP +type RDAPLink struct { + Value string `json:"value,omitempty"` + Rel string `json:"rel,omitempty"` + Href string `json:"href,omitempty"` + Type string `json:"type,omitempty"` +} + +// RDAPRemark represents a remark/notice +type RDAPRemark struct { + Title string `json:"title,omitempty"` + Description []string `json:"description,omitempty"` + Links []RDAPLink `json:"links,omitempty"` +} + +// RDAPNotice is an alias for RDAPRemark +type RDAPNotice = RDAPRemark + +// RDAPBootstrapRegistry holds the RDAP bootstrap data +type RDAPBootstrapRegistry struct { + Services [][]interface{} `json:"services"` + Version string `json:"version"` +} + +// RDAP server URLs for different TLDs and RIRs +var rdapServers = map[string]string{ + // Generic TLDs (ICANN) + "com": "https://rdap.verisign.com/com/v1/", + "net": "https://rdap.verisign.com/net/v1/", + "org": "https://rdap.publicinterestregistry.org/rdap/", + "info": "https://rdap.afilias.net/rdap/info/", + "biz": "https://rdap.afilias.net/rdap/biz/", + "io": "https://rdap.nic.io/", + "co": "https://rdap.nic.co/", + "me": "https://rdap.nic.me/", + "app": "https://rdap.nic.google/", + "dev": "https://rdap.nic.google/", + + // Country code TLDs + "uk": "https://rdap.nominet.uk/uk/", + "de": "https://rdap.denic.de/", + "nl": "https://rdap.sidn.nl/", + "au": "https://rdap.auda.org.au/", + "nz": "https://rdap.dns.net.nz/", + "br": "https://rdap.registro.br/", + "jp": "https://rdap.jprs.jp/", + + // RIRs for IP lookups + "arin": "https://rdap.arin.net/registry/", + "ripe": "https://rdap.db.ripe.net/", + "apnic": "https://rdap.apnic.net/", + "afrinic": "https://rdap.afrinic.net/rdap/", + "lacnic": "https://rdap.lacnic.net/rdap/", +} + +// RDAPLookupDomain performs an RDAP lookup for a domain +func RDAPLookupDomain(domain string) RDAPResponse { + return RDAPLookupDomainWithTimeout(domain, 15*time.Second) +} + +// RDAPLookupDomainWithTimeout performs an RDAP lookup with custom timeout +func RDAPLookupDomainWithTimeout(domain string, timeout time.Duration) RDAPResponse { + start := time.Now() + result := RDAPResponse{ + LDHName: domain, + Timestamp: start, + } + + // Extract TLD + parts := strings.Split(strings.ToLower(domain), ".") + if len(parts) < 2 { + result.Error = "invalid domain format" + result.LookupTimeMs = time.Since(start).Milliseconds() + return result + } + tld := parts[len(parts)-1] + + // Find RDAP server + serverURL, ok := rdapServers[tld] + if !ok { + // Try to use IANA bootstrap + serverURL = fmt.Sprintf("https://rdap.org/domain/%s", domain) + } else { + serverURL = serverURL + "domain/" + domain + } + + client := &http.Client{Timeout: timeout} + resp, err := client.Get(serverURL) + if err != nil { + result.Error = fmt.Sprintf("RDAP request failed: %s", err.Error()) + result.LookupTimeMs = time.Since(start).Milliseconds() + return result + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + result.Error = fmt.Sprintf("failed to read response: %s", err.Error()) + result.LookupTimeMs = time.Since(start).Milliseconds() + return result + } + + result.RawJSON = string(body) + + if resp.StatusCode != http.StatusOK { + result.Error = fmt.Sprintf("RDAP server returned status %d", resp.StatusCode) + result.LookupTimeMs = time.Since(start).Milliseconds() + return result + } + + if err := json.Unmarshal(body, &result); err != nil { + result.Error = fmt.Sprintf("failed to parse RDAP response: %s", err.Error()) + } + + result.LookupTimeMs = time.Since(start).Milliseconds() + return result +} + +// RDAPLookupIP performs an RDAP lookup for an IP address +func RDAPLookupIP(ip string) RDAPResponse { + return RDAPLookupIPWithTimeout(ip, 15*time.Second) +} + +// RDAPLookupIPWithTimeout performs an RDAP lookup for an IP with custom timeout +func RDAPLookupIPWithTimeout(ip string, timeout time.Duration) RDAPResponse { + start := time.Now() + result := RDAPResponse{ + StartAddress: ip, + Timestamp: start, + } + + parsedIP := net.ParseIP(ip) + if parsedIP == nil { + result.Error = "invalid IP address" + result.LookupTimeMs = time.Since(start).Milliseconds() + return result + } + + // Use rdap.org as a universal redirector + serverURL := fmt.Sprintf("https://rdap.org/ip/%s", ip) + + client := &http.Client{Timeout: timeout} + resp, err := client.Get(serverURL) + if err != nil { + result.Error = fmt.Sprintf("RDAP request failed: %s", err.Error()) + result.LookupTimeMs = time.Since(start).Milliseconds() + return result + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + result.Error = fmt.Sprintf("failed to read response: %s", err.Error()) + result.LookupTimeMs = time.Since(start).Milliseconds() + return result + } + + result.RawJSON = string(body) + + if resp.StatusCode != http.StatusOK { + result.Error = fmt.Sprintf("RDAP server returned status %d", resp.StatusCode) + result.LookupTimeMs = time.Since(start).Milliseconds() + return result + } + + if err := json.Unmarshal(body, &result); err != nil { + result.Error = fmt.Sprintf("failed to parse RDAP response: %s", err.Error()) + } + + result.LookupTimeMs = time.Since(start).Milliseconds() + return result +} + +// RDAPLookupASN performs an RDAP lookup for an ASN +func RDAPLookupASN(asn string) RDAPResponse { + return RDAPLookupASNWithTimeout(asn, 15*time.Second) +} + +// RDAPLookupASNWithTimeout performs an RDAP lookup for an ASN with timeout +func RDAPLookupASNWithTimeout(asn string, timeout time.Duration) RDAPResponse { + start := time.Now() + result := RDAPResponse{ + Handle: asn, + Timestamp: start, + } + + // Normalize ASN (remove "AS" prefix if present) + asnNum := strings.TrimPrefix(strings.ToUpper(asn), "AS") + + // Use rdap.org as a universal redirector + serverURL := fmt.Sprintf("https://rdap.org/autnum/%s", asnNum) + + client := &http.Client{Timeout: timeout} + resp, err := client.Get(serverURL) + if err != nil { + result.Error = fmt.Sprintf("RDAP request failed: %s", err.Error()) + result.LookupTimeMs = time.Since(start).Milliseconds() + return result + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + result.Error = fmt.Sprintf("failed to read response: %s", err.Error()) + result.LookupTimeMs = time.Since(start).Milliseconds() + return result + } + + result.RawJSON = string(body) + + if resp.StatusCode != http.StatusOK { + result.Error = fmt.Sprintf("RDAP server returned status %d", resp.StatusCode) + result.LookupTimeMs = time.Since(start).Milliseconds() + return result + } + + if err := json.Unmarshal(body, &result); err != nil { + result.Error = fmt.Sprintf("failed to parse RDAP response: %s", err.Error()) + } + + result.LookupTimeMs = time.Since(start).Milliseconds() + return result +} + +// ============================================================================ +// External Tool Links +// ============================================================================ + +// ExternalToolLinks contains links to external DNS/network analysis tools +type ExternalToolLinks struct { + // Target being analyzed + Target string `json:"target"` + Type string `json:"type"` // "domain", "ip", "email" + + // MXToolbox links + MXToolboxDNS string `json:"mxtoolboxDns,omitempty"` + MXToolboxMX string `json:"mxtoolboxMx,omitempty"` + MXToolboxBlacklist string `json:"mxtoolboxBlacklist,omitempty"` + MXToolboxSMTP string `json:"mxtoolboxSmtp,omitempty"` + MXToolboxSPF string `json:"mxtoolboxSpf,omitempty"` + MXToolboxDMARC string `json:"mxtoolboxDmarc,omitempty"` + MXToolboxDKIM string `json:"mxtoolboxDkim,omitempty"` + MXToolboxHTTP string `json:"mxtoolboxHttp,omitempty"` + MXToolboxHTTPS string `json:"mxtoolboxHttps,omitempty"` + MXToolboxPing string `json:"mxtoolboxPing,omitempty"` + MXToolboxTrace string `json:"mxtoolboxTrace,omitempty"` + MXToolboxWhois string `json:"mxtoolboxWhois,omitempty"` + MXToolboxASN string `json:"mxtoolboxAsn,omitempty"` + + // DNSChecker links + DNSCheckerDNS string `json:"dnscheckerDns,omitempty"` + DNSCheckerPropagation string `json:"dnscheckerPropagation,omitempty"` + + // Other tools + WhoIs string `json:"whois,omitempty"` + ViewDNS string `json:"viewdns,omitempty"` + IntoDNS string `json:"intodns,omitempty"` + DNSViz string `json:"dnsviz,omitempty"` + SecurityTrails string `json:"securitytrails,omitempty"` + Shodan string `json:"shodan,omitempty"` + Censys string `json:"censys,omitempty"` + BuiltWith string `json:"builtwith,omitempty"` + SSLLabs string `json:"ssllabs,omitempty"` + HSTSPreload string `json:"hstsPreload,omitempty"` + Hardenize string `json:"hardenize,omitempty"` + + // IP-specific tools + IPInfo string `json:"ipinfo,omitempty"` + AbuseIPDB string `json:"abuseipdb,omitempty"` + VirusTotal string `json:"virustotal,omitempty"` + ThreatCrowd string `json:"threatcrowd,omitempty"` + + // Email-specific tools + MailTester string `json:"mailtester,omitempty"` + LearnDMARC string `json:"learndmarc,omitempty"` +} + +// GetExternalToolLinks generates links to external analysis tools for a domain +func GetExternalToolLinks(domain string) ExternalToolLinks { + encoded := url.QueryEscape(domain) + + return ExternalToolLinks{ + Target: domain, + Type: "domain", + + // MXToolbox + MXToolboxDNS: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=dns%%3a%s&run=toolpage", encoded), + MXToolboxMX: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=mx%%3a%s&run=toolpage", encoded), + MXToolboxBlacklist: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=blacklist%%3a%s&run=toolpage", encoded), + MXToolboxSMTP: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=smtp%%3a%s&run=toolpage", encoded), + MXToolboxSPF: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=spf%%3a%s&run=toolpage", encoded), + MXToolboxDMARC: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=dmarc%%3a%s&run=toolpage", encoded), + MXToolboxDKIM: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=dkim%%3a%s&run=toolpage", encoded), + MXToolboxHTTP: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=http%%3a%s&run=toolpage", encoded), + MXToolboxHTTPS: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=https%%3a%s&run=toolpage", encoded), + MXToolboxPing: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=ping%%3a%s&run=toolpage", encoded), + MXToolboxTrace: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=trace%%3a%s&run=toolpage", encoded), + MXToolboxWhois: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=whois%%3a%s&run=toolpage", encoded), + + // DNSChecker + DNSCheckerDNS: fmt.Sprintf("https://dnschecker.org/#A/%s", encoded), + DNSCheckerPropagation: fmt.Sprintf("https://dnschecker.org/dns-propagation.php?domain=%s", encoded), + + // Other tools + WhoIs: fmt.Sprintf("https://who.is/whois/%s", encoded), + ViewDNS: fmt.Sprintf("https://viewdns.info/dnsrecord/?domain=%s", encoded), + IntoDNS: fmt.Sprintf("https://intodns.com/%s", encoded), + DNSViz: fmt.Sprintf("https://dnsviz.net/d/%s/analyze/", encoded), + SecurityTrails: fmt.Sprintf("https://securitytrails.com/domain/%s", encoded), + BuiltWith: fmt.Sprintf("https://builtwith.com/%s", encoded), + SSLLabs: fmt.Sprintf("https://www.ssllabs.com/ssltest/analyze.html?d=%s", encoded), + HSTSPreload: fmt.Sprintf("https://hstspreload.org/?domain=%s", encoded), + Hardenize: fmt.Sprintf("https://www.hardenize.com/report/%s", encoded), + VirusTotal: fmt.Sprintf("https://www.virustotal.com/gui/domain/%s", encoded), + } +} + +// GetExternalToolLinksIP generates links to external analysis tools for an IP +func GetExternalToolLinksIP(ip string) ExternalToolLinks { + encoded := url.QueryEscape(ip) + + return ExternalToolLinks{ + Target: ip, + Type: "ip", + + // MXToolbox + MXToolboxBlacklist: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=blacklist%%3a%s&run=toolpage", encoded), + MXToolboxPing: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=ping%%3a%s&run=toolpage", encoded), + MXToolboxTrace: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=trace%%3a%s&run=toolpage", encoded), + MXToolboxWhois: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=whois%%3a%s&run=toolpage", encoded), + MXToolboxASN: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=asn%%3a%s&run=toolpage", encoded), + + // IP-specific tools + IPInfo: fmt.Sprintf("https://ipinfo.io/%s", encoded), + AbuseIPDB: fmt.Sprintf("https://www.abuseipdb.com/check/%s", encoded), + VirusTotal: fmt.Sprintf("https://www.virustotal.com/gui/ip-address/%s", encoded), + Shodan: fmt.Sprintf("https://www.shodan.io/host/%s", encoded), + Censys: fmt.Sprintf("https://search.censys.io/hosts/%s", encoded), + ThreatCrowd: fmt.Sprintf("https://www.threatcrowd.org/ip.php?ip=%s", encoded), + } +} + +// GetExternalToolLinksEmail generates links for email-related checks +func GetExternalToolLinksEmail(emailOrDomain string) ExternalToolLinks { + // Extract domain from email if needed + domain := emailOrDomain + if strings.Contains(emailOrDomain, "@") { + parts := strings.Split(emailOrDomain, "@") + if len(parts) == 2 { + domain = parts[1] + } + } + + encoded := url.QueryEscape(domain) + emailEncoded := url.QueryEscape(emailOrDomain) + + return ExternalToolLinks{ + Target: emailOrDomain, + Type: "email", + + // MXToolbox email checks + MXToolboxMX: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=mx%%3a%s&run=toolpage", encoded), + MXToolboxSMTP: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=smtp%%3a%s&run=toolpage", encoded), + MXToolboxSPF: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=spf%%3a%s&run=toolpage", encoded), + MXToolboxDMARC: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=dmarc%%3a%s&run=toolpage", encoded), + MXToolboxDKIM: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=dkim%%3a%s&run=toolpage", encoded), + + // Email-specific tools + MailTester: fmt.Sprintf("https://www.mail-tester.com/test-%s", emailEncoded), + LearnDMARC: fmt.Sprintf("https://www.learndmarc.com/?domain=%s", encoded), + } +} + +// ============================================================================ +// Convenience Types for Parsed Results +// ============================================================================ + +// ParsedDomainInfo provides a simplified view of domain information +type ParsedDomainInfo struct { + Domain string `json:"domain"` + Registrar string `json:"registrar,omitempty"` + RegistrationDate string `json:"registrationDate,omitempty"` + ExpirationDate string `json:"expirationDate,omitempty"` + UpdatedDate string `json:"updatedDate,omitempty"` + Status []string `json:"status,omitempty"` + Nameservers []string `json:"nameservers,omitempty"` + DNSSEC bool `json:"dnssec"` +} + +// ParseRDAPResponse extracts key information from an RDAP response +func ParseRDAPResponse(resp RDAPResponse) ParsedDomainInfo { + info := ParsedDomainInfo{ + Domain: resp.LDHName, + Status: resp.Status, + } + + // Extract dates from events + for _, event := range resp.Events { + switch event.EventAction { + case "registration": + info.RegistrationDate = event.EventDate + case "expiration": + info.ExpirationDate = event.EventDate + case "last changed", "last update": + info.UpdatedDate = event.EventDate + } + } + + // Extract registrar from entities + for _, entity := range resp.Entities { + for _, role := range entity.Roles { + if role == "registrar" { + info.Registrar = entity.Handle + break + } + } + } + + // Extract nameservers + for _, ns := range resp.Nameservers { + info.Nameservers = append(info.Nameservers, ns.LDHName) + } + + // Check for DNSSEC + for _, status := range resp.Status { + if strings.Contains(strings.ToLower(status), "dnssec") || + strings.Contains(strings.ToLower(status), "signed") { + info.DNSSEC = true + break + } + } + + return info +} diff --git a/dns_tools_test.go b/dns_tools_test.go new file mode 100644 index 0000000..91e830d --- /dev/null +++ b/dns_tools_test.go @@ -0,0 +1,467 @@ +package poindexter + +import ( + "strings" + "testing" +) + +// ============================================================================ +// External Tool Links Tests +// ============================================================================ + +func TestGetExternalToolLinks(t *testing.T) { + links := GetExternalToolLinks("example.com") + + if links.Target != "example.com" { + t.Errorf("expected target=example.com, got %s", links.Target) + } + if links.Type != "domain" { + t.Errorf("expected type=domain, got %s", links.Type) + } + + // Check MXToolbox links + if !strings.Contains(links.MXToolboxDNS, "mxtoolbox.com") { + t.Error("MXToolboxDNS should contain mxtoolbox.com") + } + if !strings.Contains(links.MXToolboxDNS, "example.com") { + t.Error("MXToolboxDNS should contain the domain") + } + + if !strings.Contains(links.MXToolboxMX, "mxtoolbox.com") { + t.Error("MXToolboxMX should contain mxtoolbox.com") + } + + if !strings.Contains(links.MXToolboxSPF, "spf") { + t.Error("MXToolboxSPF should contain 'spf'") + } + + if !strings.Contains(links.MXToolboxDMARC, "dmarc") { + t.Error("MXToolboxDMARC should contain 'dmarc'") + } + + // Check DNSChecker links + if !strings.Contains(links.DNSCheckerDNS, "dnschecker.org") { + t.Error("DNSCheckerDNS should contain dnschecker.org") + } + + // Check other tools + if !strings.Contains(links.WhoIs, "who.is") { + t.Error("WhoIs should contain who.is") + } + + if !strings.Contains(links.SSLLabs, "ssllabs.com") { + t.Error("SSLLabs should contain ssllabs.com") + } + + if !strings.Contains(links.VirusTotal, "virustotal.com") { + t.Error("VirusTotal should contain virustotal.com") + } +} + +func TestGetExternalToolLinksIP(t *testing.T) { + links := GetExternalToolLinksIP("8.8.8.8") + + if links.Target != "8.8.8.8" { + t.Errorf("expected target=8.8.8.8, got %s", links.Target) + } + if links.Type != "ip" { + t.Errorf("expected type=ip, got %s", links.Type) + } + + // Check IP-specific links + if !strings.Contains(links.IPInfo, "ipinfo.io") { + t.Error("IPInfo should contain ipinfo.io") + } + if !strings.Contains(links.IPInfo, "8.8.8.8") { + t.Error("IPInfo should contain the IP address") + } + + if !strings.Contains(links.AbuseIPDB, "abuseipdb.com") { + t.Error("AbuseIPDB should contain abuseipdb.com") + } + + if !strings.Contains(links.Shodan, "shodan.io") { + t.Error("Shodan should contain shodan.io") + } + + if !strings.Contains(links.MXToolboxBlacklist, "blacklist") { + t.Error("MXToolboxBlacklist should contain 'blacklist'") + } +} + +func TestGetExternalToolLinksEmail(t *testing.T) { + // Test with email address + links := GetExternalToolLinksEmail("test@example.com") + + if links.Target != "test@example.com" { + t.Errorf("expected target=test@example.com, got %s", links.Target) + } + if links.Type != "email" { + t.Errorf("expected type=email, got %s", links.Type) + } + + // Email tools should use the domain + if !strings.Contains(links.MXToolboxMX, "example.com") { + t.Error("MXToolboxMX should contain the domain from email") + } + + if !strings.Contains(links.MXToolboxSPF, "spf") { + t.Error("MXToolboxSPF should contain 'spf'") + } + + if !strings.Contains(links.MXToolboxDMARC, "dmarc") { + t.Error("MXToolboxDMARC should contain 'dmarc'") + } + + // Test with just domain + links2 := GetExternalToolLinksEmail("example.org") + if links2.Target != "example.org" { + t.Errorf("expected target=example.org, got %s", links2.Target) + } +} + +func TestGetExternalToolLinksSpecialChars(t *testing.T) { + // Test URL encoding + links := GetExternalToolLinks("test-domain.example.com") + + if !strings.Contains(links.MXToolboxDNS, "test-domain.example.com") { + t.Error("Should handle hyphens in domain") + } +} + +// ============================================================================ +// DNS Lookup Tests (Unit tests for structure, not network) +// ============================================================================ + +func TestDNSRecordTypes(t *testing.T) { + types := []DNSRecordType{ + DNSRecordA, + DNSRecordAAAA, + DNSRecordMX, + DNSRecordTXT, + DNSRecordNS, + DNSRecordCNAME, + DNSRecordSOA, + DNSRecordPTR, + DNSRecordSRV, + DNSRecordCAA, + } + + expected := []string{"A", "AAAA", "MX", "TXT", "NS", "CNAME", "SOA", "PTR", "SRV", "CAA"} + + for i, typ := range types { + if string(typ) != expected[i] { + t.Errorf("expected type %s, got %s", expected[i], typ) + } + } +} + +func TestDNSLookupResultStructure(t *testing.T) { + result := DNSLookupResult{ + Domain: "example.com", + QueryType: "A", + Records: []DNSRecord{ + {Type: DNSRecordA, Name: "example.com", Value: "93.184.216.34"}, + }, + LookupTimeMs: 50, + } + + if result.Domain != "example.com" { + t.Error("Domain should be set") + } + if len(result.Records) != 1 { + t.Error("Should have 1 record") + } + if result.Records[0].Type != DNSRecordA { + t.Error("Record type should be A") + } +} + +func TestCompleteDNSLookupStructure(t *testing.T) { + result := CompleteDNSLookup{ + Domain: "example.com", + A: []string{"93.184.216.34"}, + AAAA: []string{"2606:2800:220:1:248:1893:25c8:1946"}, + MX: []MXRecord{ + {Host: "mail.example.com", Priority: 10}, + }, + NS: []string{"ns1.example.com", "ns2.example.com"}, + TXT: []string{"v=spf1 include:_spf.example.com ~all"}, + } + + if result.Domain != "example.com" { + t.Error("Domain should be set") + } + if len(result.A) != 1 { + t.Error("Should have 1 A record") + } + if len(result.AAAA) != 1 { + t.Error("Should have 1 AAAA record") + } + if len(result.MX) != 1 { + t.Error("Should have 1 MX record") + } + if result.MX[0].Priority != 10 { + t.Error("MX priority should be 10") + } + if len(result.NS) != 2 { + t.Error("Should have 2 NS records") + } +} + +// ============================================================================ +// RDAP Tests (Unit tests for structure, not network) +// ============================================================================ + +func TestRDAPResponseStructure(t *testing.T) { + resp := RDAPResponse{ + LDHName: "example.com", + Status: []string{"active", "client transfer prohibited"}, + Events: []RDAPEvent{ + {EventAction: "registration", EventDate: "2020-01-01T00:00:00Z"}, + {EventAction: "expiration", EventDate: "2025-01-01T00:00:00Z"}, + }, + Entities: []RDAPEntity{ + {Handle: "REGISTRAR-1", Roles: []string{"registrar"}}, + }, + Nameservers: []RDAPNs{ + {LDHName: "ns1.example.com"}, + {LDHName: "ns2.example.com"}, + }, + } + + if resp.LDHName != "example.com" { + t.Error("LDHName should be set") + } + if len(resp.Status) != 2 { + t.Error("Should have 2 status values") + } + if len(resp.Events) != 2 { + t.Error("Should have 2 events") + } + if resp.Events[0].EventAction != "registration" { + t.Error("First event should be registration") + } + if len(resp.Nameservers) != 2 { + t.Error("Should have 2 nameservers") + } +} + +func TestParseRDAPResponse(t *testing.T) { + resp := RDAPResponse{ + LDHName: "example.com", + Status: []string{"active", "dnssecSigned"}, + Events: []RDAPEvent{ + {EventAction: "registration", EventDate: "2020-01-01T00:00:00Z"}, + {EventAction: "expiration", EventDate: "2025-01-01T00:00:00Z"}, + {EventAction: "last changed", EventDate: "2024-06-15T00:00:00Z"}, + }, + Entities: []RDAPEntity{ + {Handle: "REGISTRAR-123", Roles: []string{"registrar"}}, + }, + Nameservers: []RDAPNs{ + {LDHName: "ns1.example.com"}, + {LDHName: "ns2.example.com"}, + }, + } + + info := ParseRDAPResponse(resp) + + if info.Domain != "example.com" { + t.Errorf("expected domain=example.com, got %s", info.Domain) + } + if info.RegistrationDate != "2020-01-01T00:00:00Z" { + t.Errorf("expected registration date, got %s", info.RegistrationDate) + } + if info.ExpirationDate != "2025-01-01T00:00:00Z" { + t.Errorf("expected expiration date, got %s", info.ExpirationDate) + } + if info.UpdatedDate != "2024-06-15T00:00:00Z" { + t.Errorf("expected updated date, got %s", info.UpdatedDate) + } + if info.Registrar != "REGISTRAR-123" { + t.Errorf("expected registrar, got %s", info.Registrar) + } + if len(info.Nameservers) != 2 { + t.Error("Should have 2 nameservers") + } + if !info.DNSSEC { + t.Error("DNSSEC should be true (detected from status)") + } +} + +func TestParseRDAPResponseEmpty(t *testing.T) { + resp := RDAPResponse{ + LDHName: "test.com", + } + + info := ParseRDAPResponse(resp) + + if info.Domain != "test.com" { + t.Error("Domain should be set even with minimal response") + } + if info.DNSSEC { + t.Error("DNSSEC should be false with no status") + } + if len(info.Nameservers) != 0 { + t.Error("Nameservers should be empty") + } +} + +// ============================================================================ +// RDAP Server Tests +// ============================================================================ + +func TestRDAPServers(t *testing.T) { + // Check that we have servers for common TLDs + commonTLDs := []string{"com", "net", "org", "io"} + for _, tld := range commonTLDs { + if _, ok := rdapServers[tld]; !ok { + t.Errorf("missing RDAP server for TLD: %s", tld) + } + } + + // Check RIRs + rirs := []string{"arin", "ripe", "apnic", "afrinic", "lacnic"} + for _, rir := range rirs { + if _, ok := rdapServers[rir]; !ok { + t.Errorf("missing RDAP server for RIR: %s", rir) + } + } +} + +// ============================================================================ +// MX Record Tests +// ============================================================================ + +func TestMXRecordStructure(t *testing.T) { + mx := MXRecord{ + Host: "mail.example.com", + Priority: 10, + } + + if mx.Host != "mail.example.com" { + t.Error("Host should be set") + } + if mx.Priority != 10 { + t.Error("Priority should be 10") + } +} + +// ============================================================================ +// SRV Record Tests +// ============================================================================ + +func TestSRVRecordStructure(t *testing.T) { + srv := SRVRecord{ + Target: "sipserver.example.com", + Port: 5060, + Priority: 10, + Weight: 100, + } + + if srv.Target != "sipserver.example.com" { + t.Error("Target should be set") + } + if srv.Port != 5060 { + t.Error("Port should be 5060") + } + if srv.Priority != 10 { + t.Error("Priority should be 10") + } + if srv.Weight != 100 { + t.Error("Weight should be 100") + } +} + +// ============================================================================ +// SOA Record Tests +// ============================================================================ + +func TestSOARecordStructure(t *testing.T) { + soa := SOARecord{ + PrimaryNS: "ns1.example.com", + AdminEmail: "admin.example.com", + Serial: 2024010101, + Refresh: 7200, + Retry: 3600, + Expire: 1209600, + MinTTL: 86400, + } + + if soa.PrimaryNS != "ns1.example.com" { + t.Error("PrimaryNS should be set") + } + if soa.Serial != 2024010101 { + t.Error("Serial should match") + } + if soa.Refresh != 7200 { + t.Error("Refresh should be 7200") + } +} + +// ============================================================================ +// Helper Function Tests +// ============================================================================ + +func TestIsNoSuchHostError(t *testing.T) { + tests := []struct { + errStr string + expected bool + }{ + {"no such host", true}, + {"NXDOMAIN", true}, + {"not found", true}, + {"connection refused", false}, + {"timeout", false}, + {"", false}, + } + + for _, tc := range tests { + var err error + if tc.errStr != "" { + err = &testError{msg: tc.errStr} + } + result := isNoSuchHostError(err) + if result != tc.expected { + t.Errorf("isNoSuchHostError(%q) = %v, want %v", tc.errStr, result, tc.expected) + } + } +} + +type testError struct { + msg string +} + +func (e *testError) Error() string { + return e.msg +} + +// ============================================================================ +// URL Building Tests +// ============================================================================ + +func TestBuildRDAPURLs(t *testing.T) { + // These test the URL structure, not actual lookups + + // Domain URL + domain := "example.com" + expectedDomainPrefix := "https://rdap.org/domain/" + if !strings.HasPrefix("https://rdap.org/domain/"+domain, expectedDomainPrefix) { + t.Error("Domain URL format is incorrect") + } + + // IP URL + ip := "8.8.8.8" + expectedIPPrefix := "https://rdap.org/ip/" + if !strings.HasPrefix("https://rdap.org/ip/"+ip, expectedIPPrefix) { + t.Error("IP URL format is incorrect") + } + + // ASN URL + asn := "15169" + expectedASNPrefix := "https://rdap.org/autnum/" + if !strings.HasPrefix("https://rdap.org/autnum/"+asn, expectedASNPrefix) { + t.Error("ASN URL format is incorrect") + } +} diff --git a/npm/poindexter-wasm/index.d.ts b/npm/poindexter-wasm/index.d.ts index 1ef2644..8837fe9 100644 --- a/npm/poindexter-wasm/index.d.ts +++ b/npm/poindexter-wasm/index.d.ts @@ -189,6 +189,216 @@ export interface InitOptions { instantiateWasm?: (source: ArrayBuffer, importObject: WebAssembly.Imports) => Promise | WebAssembly.Instance; } +// ============================================================================ +// DNS Tools Types +// ============================================================================ + +/** DNS record types */ +export type DNSRecordType = 'A' | 'AAAA' | 'MX' | 'TXT' | 'NS' | 'CNAME' | 'SOA' | 'PTR' | 'SRV' | 'CAA'; + +/** External tool links for domain/IP/email analysis */ +export interface ExternalToolLinks { + target: string; + type: 'domain' | 'ip' | 'email'; + + // MXToolbox links + mxtoolboxDns?: string; + mxtoolboxMx?: string; + mxtoolboxBlacklist?: string; + mxtoolboxSmtp?: string; + mxtoolboxSpf?: string; + mxtoolboxDmarc?: string; + mxtoolboxDkim?: string; + mxtoolboxHttp?: string; + mxtoolboxHttps?: string; + mxtoolboxPing?: string; + mxtoolboxTrace?: string; + mxtoolboxWhois?: string; + mxtoolboxAsn?: string; + + // DNSChecker links + dnscheckerDns?: string; + dnscheckerPropagation?: string; + + // Other tools + whois?: string; + viewdns?: string; + intodns?: string; + dnsviz?: string; + securitytrails?: string; + shodan?: string; + censys?: string; + builtwith?: string; + ssllabs?: string; + hstsPreload?: string; + hardenize?: string; + + // IP-specific tools + ipinfo?: string; + abuseipdb?: string; + virustotal?: string; + threatcrowd?: string; + + // Email-specific tools + mailtester?: string; + learndmarc?: string; +} + +/** RDAP server registry */ +export interface RDAPServers { + tlds: Record; + rirs: Record; + universal: string; +} + +/** RDAP response event */ +export interface RDAPEvent { + eventAction: string; + eventDate: string; + eventActor?: string; +} + +/** RDAP entity (registrar, registrant, etc.) */ +export interface RDAPEntity { + handle?: string; + roles?: string[]; + vcardArray?: any[]; + entities?: RDAPEntity[]; + events?: RDAPEvent[]; +} + +/** RDAP nameserver */ +export interface RDAPNameserver { + ldhName: string; + ipAddresses?: { + v4?: string[]; + v6?: string[]; + }; +} + +/** RDAP link */ +export interface RDAPLink { + value?: string; + rel?: string; + href?: string; + type?: string; +} + +/** RDAP remark/notice */ +export interface RDAPRemark { + title?: string; + description?: string[]; + links?: RDAPLink[]; +} + +/** RDAP response (for domain, IP, or ASN lookups) */ +export interface RDAPResponse { + // Common fields + handle?: string; + ldhName?: string; + unicodeName?: string; + status?: string[]; + events?: RDAPEvent[]; + entities?: RDAPEntity[]; + nameservers?: RDAPNameserver[]; + links?: RDAPLink[]; + remarks?: RDAPRemark[]; + notices?: RDAPRemark[]; + + // Network-specific (for IP lookups) + startAddress?: string; + endAddress?: string; + ipVersion?: string; + name?: string; + type?: string; + country?: string; + parentHandle?: string; + + // Error fields + errorCode?: number; + title?: string; + description?: string[]; + + // Metadata + rawJson?: string; + lookupTimeMs: number; + timestamp: string; + error?: string; +} + +/** Parsed domain info from RDAP */ +export interface ParsedDomainInfo { + domain: string; + registrar?: string; + registrationDate?: string; + expirationDate?: string; + updatedDate?: string; + status?: string[]; + nameservers?: string[]; + dnssec: boolean; +} + +/** DNS lookup result */ +export interface DNSLookupResult { + domain: string; + queryType: string; + records: DNSRecord[]; + mxRecords?: MXRecord[]; + srvRecords?: SRVRecord[]; + soaRecord?: SOARecord; + lookupTimeMs: number; + error?: string; + timestamp: string; +} + +/** DNS record */ +export interface DNSRecord { + type: DNSRecordType; + name: string; + value: string; + ttl?: number; +} + +/** MX record */ +export interface MXRecord { + host: string; + priority: number; +} + +/** SRV record */ +export interface SRVRecord { + target: string; + port: number; + priority: number; + weight: number; +} + +/** SOA record */ +export interface SOARecord { + primaryNs: string; + adminEmail: string; + serial: number; + refresh: number; + retry: number; + expire: number; + minTtl: number; +} + +/** Complete DNS lookup result */ +export interface CompleteDNSLookup { + domain: string; + a?: string[]; + aaaa?: string[]; + mx?: MXRecord[]; + ns?: string[]; + txt?: string[]; + cname?: string; + soa?: SOARecord; + lookupTimeMs: number; + errors?: string[]; + timestamp: string; +} + // ============================================================================ // Main API // ============================================================================ @@ -209,6 +419,16 @@ export interface PxAPI { getDefaultPeerFeatureRanges(): Promise; normalizePeerFeatures(features: number[], ranges?: FeatureRanges): Promise; weightedPeerFeatures(normalized: number[], weights: number[]): Promise; + + // DNS tools + getExternalToolLinks(domain: string): Promise; + getExternalToolLinksIP(ip: string): Promise; + getExternalToolLinksEmail(emailOrDomain: string): Promise; + getRDAPServers(): Promise; + buildRDAPDomainURL(domain: string): Promise; + buildRDAPIPURL(ip: string): Promise; + buildRDAPASNURL(asn: string): Promise; + getDNSRecordTypes(): Promise; } export function init(options?: InitOptions): Promise; diff --git a/npm/poindexter-wasm/loader.js b/npm/poindexter-wasm/loader.js index 25754f8..3d32d2c 100644 --- a/npm/poindexter-wasm/loader.js +++ b/npm/poindexter-wasm/loader.js @@ -100,7 +100,16 @@ export async function init(options = {}) { getDefaultQualityWeights: async () => call('pxGetDefaultQualityWeights'), getDefaultPeerFeatureRanges: async () => call('pxGetDefaultPeerFeatureRanges'), normalizePeerFeatures: async (features, ranges) => call('pxNormalizePeerFeatures', features, ranges), - weightedPeerFeatures: async (normalized, weights) => call('pxWeightedPeerFeatures', normalized, weights) + weightedPeerFeatures: async (normalized, weights) => call('pxWeightedPeerFeatures', normalized, weights), + // DNS tools + getExternalToolLinks: async (domain) => call('pxGetExternalToolLinks', domain), + getExternalToolLinksIP: async (ip) => call('pxGetExternalToolLinksIP', ip), + getExternalToolLinksEmail: async (emailOrDomain) => call('pxGetExternalToolLinksEmail', emailOrDomain), + getRDAPServers: async () => call('pxGetRDAPServers'), + buildRDAPDomainURL: async (domain) => call('pxBuildRDAPDomainURL', domain), + buildRDAPIPURL: async (ip) => call('pxBuildRDAPIPURL', ip), + buildRDAPASNURL: async (asn) => call('pxBuildRDAPASNURL', asn), + getDNSRecordTypes: async () => call('pxGetDNSRecordTypes') }; return api; diff --git a/wasm/main.go b/wasm/main.go index 6fd9019..a124ad0 100644 --- a/wasm/main.go +++ b/wasm/main.go @@ -527,6 +527,149 @@ func weightedPeerFeatures(_ js.Value, args []js.Value) (any, error) { return weighted, nil } +// ============================================================================ +// DNS Tools Functions +// ============================================================================ + +func getExternalToolLinks(_ js.Value, args []js.Value) (any, error) { + // getExternalToolLinks(domain: string) -> ExternalToolLinks + if len(args) < 1 { + return nil, errors.New("getExternalToolLinks(domain)") + } + domain := args[0].String() + links := pd.GetExternalToolLinks(domain) + return externalToolLinksToJS(links), nil +} + +func getExternalToolLinksIP(_ js.Value, args []js.Value) (any, error) { + // getExternalToolLinksIP(ip: string) -> ExternalToolLinks + if len(args) < 1 { + return nil, errors.New("getExternalToolLinksIP(ip)") + } + ip := args[0].String() + links := pd.GetExternalToolLinksIP(ip) + return externalToolLinksToJS(links), nil +} + +func getExternalToolLinksEmail(_ js.Value, args []js.Value) (any, error) { + // getExternalToolLinksEmail(emailOrDomain: string) -> ExternalToolLinks + if len(args) < 1 { + return nil, errors.New("getExternalToolLinksEmail(emailOrDomain)") + } + emailOrDomain := args[0].String() + links := pd.GetExternalToolLinksEmail(emailOrDomain) + return externalToolLinksToJS(links), nil +} + +func externalToolLinksToJS(links pd.ExternalToolLinks) map[string]any { + return map[string]any{ + "target": links.Target, + "type": links.Type, + // MXToolbox + "mxtoolboxDns": links.MXToolboxDNS, + "mxtoolboxMx": links.MXToolboxMX, + "mxtoolboxBlacklist": links.MXToolboxBlacklist, + "mxtoolboxSmtp": links.MXToolboxSMTP, + "mxtoolboxSpf": links.MXToolboxSPF, + "mxtoolboxDmarc": links.MXToolboxDMARC, + "mxtoolboxDkim": links.MXToolboxDKIM, + "mxtoolboxHttp": links.MXToolboxHTTP, + "mxtoolboxHttps": links.MXToolboxHTTPS, + "mxtoolboxPing": links.MXToolboxPing, + "mxtoolboxTrace": links.MXToolboxTrace, + "mxtoolboxWhois": links.MXToolboxWhois, + "mxtoolboxAsn": links.MXToolboxASN, + // DNSChecker + "dnscheckerDns": links.DNSCheckerDNS, + "dnscheckerPropagation": links.DNSCheckerPropagation, + // Other tools + "whois": links.WhoIs, + "viewdns": links.ViewDNS, + "intodns": links.IntoDNS, + "dnsviz": links.DNSViz, + "securitytrails": links.SecurityTrails, + "shodan": links.Shodan, + "censys": links.Censys, + "builtwith": links.BuiltWith, + "ssllabs": links.SSLLabs, + "hstsPreload": links.HSTSPreload, + "hardenize": links.Hardenize, + // IP-specific + "ipinfo": links.IPInfo, + "abuseipdb": links.AbuseIPDB, + "virustotal": links.VirusTotal, + "threatcrowd": links.ThreatCrowd, + // Email-specific + "mailtester": links.MailTester, + "learndmarc": links.LearnDMARC, + } +} + +func getRDAPServers(_ js.Value, _ []js.Value) (any, error) { + // Returns a list of known RDAP servers for reference + servers := map[string]any{ + "tlds": map[string]string{ + "com": "https://rdap.verisign.com/com/v1/", + "net": "https://rdap.verisign.com/net/v1/", + "org": "https://rdap.publicinterestregistry.org/rdap/", + "info": "https://rdap.afilias.net/rdap/info/", + "io": "https://rdap.nic.io/", + "co": "https://rdap.nic.co/", + "dev": "https://rdap.nic.google/", + "app": "https://rdap.nic.google/", + }, + "rirs": map[string]string{ + "arin": "https://rdap.arin.net/registry/", + "ripe": "https://rdap.db.ripe.net/", + "apnic": "https://rdap.apnic.net/", + "afrinic": "https://rdap.afrinic.net/rdap/", + "lacnic": "https://rdap.lacnic.net/rdap/", + }, + "universal": "https://rdap.org/", + } + return servers, nil +} + +func buildRDAPDomainURL(_ js.Value, args []js.Value) (any, error) { + // buildRDAPDomainURL(domain: string) -> string + if len(args) < 1 { + return nil, errors.New("buildRDAPDomainURL(domain)") + } + domain := args[0].String() + // Use universal RDAP redirector + return fmt.Sprintf("https://rdap.org/domain/%s", domain), nil +} + +func buildRDAPIPURL(_ js.Value, args []js.Value) (any, error) { + // buildRDAPIPURL(ip: string) -> string + if len(args) < 1 { + return nil, errors.New("buildRDAPIPURL(ip)") + } + ip := args[0].String() + return fmt.Sprintf("https://rdap.org/ip/%s", ip), nil +} + +func buildRDAPASNURL(_ js.Value, args []js.Value) (any, error) { + // buildRDAPASNURL(asn: string) -> string + if len(args) < 1 { + return nil, errors.New("buildRDAPASNURL(asn)") + } + asn := args[0].String() + // Normalize ASN + asnNum := asn + if len(asn) > 2 && (asn[:2] == "AS" || asn[:2] == "as") { + asnNum = asn[2:] + } + return fmt.Sprintf("https://rdap.org/autnum/%s", asnNum), nil +} + +func getDNSRecordTypes(_ js.Value, _ []js.Value) (any, error) { + // Returns available DNS record types + return []string{ + "A", "AAAA", "MX", "TXT", "NS", "CNAME", "SOA", "PTR", "SRV", "CAA", + }, nil +} + func main() { // Export core API export("pxVersion", version) @@ -557,6 +700,16 @@ func main() { export("pxNormalizePeerFeatures", normalizePeerFeatures) export("pxWeightedPeerFeatures", weightedPeerFeatures) + // Export DNS tools API + export("pxGetExternalToolLinks", getExternalToolLinks) + export("pxGetExternalToolLinksIP", getExternalToolLinksIP) + export("pxGetExternalToolLinksEmail", getExternalToolLinksEmail) + export("pxGetRDAPServers", getRDAPServers) + export("pxBuildRDAPDomainURL", buildRDAPDomainURL) + export("pxBuildRDAPIPURL", buildRDAPIPURL) + export("pxBuildRDAPASNURL", buildRDAPASNURL) + export("pxGetDNSRecordTypes", getDNSRecordTypes) + // Keep running select {} }