feat: Add DNS tools with lookup, RDAP, and external tool links
Add comprehensive DNS tools module for network analysis: DNS Lookup functionality: - Support for A, AAAA, MX, TXT, NS, CNAME, SOA, PTR, SRV, CAA records - DNSLookup() and DNSLookupAll() for single/complete lookups - Configurable timeouts - Structured result types for all record types RDAP (new-style WHOIS) support: - RDAPLookupDomain() for domain registration data - RDAPLookupIP() for IP address information - RDAPLookupASN() for autonomous system info - Built-in server registry for common TLDs and RIRs - ParseRDAPResponse() for extracting key domain info External tool link generators: - GetExternalToolLinks() - 20+ links for domain analysis - GetExternalToolLinksIP() - IP-specific analysis tools - GetExternalToolLinksEmail() - Email/domain verification Tools include: MXToolbox (DNS, MX, SPF, DMARC, DKIM, blacklist), DNSChecker, ViewDNS, IntoDNS, DNSViz, SecurityTrails, SSL Labs, Shodan, Censys, IPInfo, AbuseIPDB, VirusTotal, and more. WASM bindings expose link generators and RDAP URL builders for use in TypeScript/browser environments.
This commit is contained in:
parent
4609b7b2bf
commit
d96c9f266c
5 changed files with 1700 additions and 1 deletions
850
dns_tools.go
Normal file
850
dns_tools.go
Normal file
|
|
@ -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
|
||||
}
|
||||
467
dns_tools_test.go
Normal file
467
dns_tools_test.go
Normal file
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
220
npm/poindexter-wasm/index.d.ts
vendored
220
npm/poindexter-wasm/index.d.ts
vendored
|
|
@ -189,6 +189,216 @@ export interface InitOptions {
|
|||
instantiateWasm?: (source: ArrayBuffer, importObject: WebAssembly.Imports) => Promise<WebAssembly.Instance> | 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<string, string>;
|
||||
rirs: Record<string, string>;
|
||||
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<FeatureRanges>;
|
||||
normalizePeerFeatures(features: number[], ranges?: FeatureRanges): Promise<number[]>;
|
||||
weightedPeerFeatures(normalized: number[], weights: number[]): Promise<number[]>;
|
||||
|
||||
// DNS tools
|
||||
getExternalToolLinks(domain: string): Promise<ExternalToolLinks>;
|
||||
getExternalToolLinksIP(ip: string): Promise<ExternalToolLinks>;
|
||||
getExternalToolLinksEmail(emailOrDomain: string): Promise<ExternalToolLinks>;
|
||||
getRDAPServers(): Promise<RDAPServers>;
|
||||
buildRDAPDomainURL(domain: string): Promise<string>;
|
||||
buildRDAPIPURL(ip: string): Promise<string>;
|
||||
buildRDAPASNURL(asn: string): Promise<string>;
|
||||
getDNSRecordTypes(): Promise<DNSRecordType[]>;
|
||||
}
|
||||
|
||||
export function init(options?: InitOptions): Promise<PxAPI>;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
153
wasm/main.go
153
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 {}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue