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.
467 lines
12 KiB
Go
467 lines
12 KiB
Go
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")
|
|
}
|
|
}
|