Skip to main content

web_analyzer/
domain_info.rs

1use regex::Regex;
2use reqwest::Client;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::time::{Duration, Instant};
6use tokio::io::{AsyncReadExt, AsyncWriteExt};
7use tokio::net::TcpStream;
8
9// ── WHOIS server database ───────────────────────────────────────────────────
10
11const WHOIS_SERVERS: &[(&str, &str)] = &[
12    // Core gTLDs
13    ("com", "whois.verisign-grs.com"),
14    ("net", "whois.verisign-grs.com"),
15    ("org", "whois.pir.org"),
16    ("edu", "whois.educause.edu"),
17    ("gov", "whois.dotgov.gov"),
18    ("mil", "whois.nic.mil"),
19    ("int", "whois.iana.org"),
20    ("info", "whois.afilias.net"),
21    ("biz", "whois.biz"),
22    ("name", "whois.nic.name"),
23    ("pro", "whois.nic.pro"),
24    ("aero", "whois.aero"),
25    ("coop", "whois.nic.coop"),
26    ("museum", "whois.museum"),
27    ("arpa", "whois.iana.org"),
28    // New Highly Active gTLDs
29    ("xyz", "whois.nic.xyz"),
30    ("top", "whois.nic.top"),
31    ("club", "whois.nic.club"),
32    ("vip", "whois.nic.vip"),
33    ("app", "whois.nic.google"),
34    ("dev", "whois.nic.google"),
35    ("shop", "whois.nic.shop"),
36    ("store", "whois.nic.store"),
37    ("site", "whois.nic.site"),
38    ("online", "whois.nic.online"),
39    ("tech", "whois.nic.tech"),
40    ("ai", "whois.nic.ai"),
41    ("io", "whois.nic.io"),
42    ("me", "whois.nic.me"),
43    ("tv", "whois.nic.tv"),
44    ("cc", "whois.nic.cc"),
45    ("website", "whois.nic.website"),
46    ("space", "whois.nic.space"),
47    ("press", "whois.nic.press"),
48    ("design", "whois.nic.design"),
49    ("agency", "whois.nic.agency"),
50    ("photography", "whois.nic.photography"),
51    ("email", "whois.nic.email"),
52    ("network", "whois.nic.network"),
53    ("today", "whois.nic.today"),
54    ("icu", "whois.nic.icu"),
55    ("wang", "whois.nic.wang"),
56    ("win", "whois.nic.win"),
57    ("mobi", "whois.nic.mobi"),
58    ("asia", "whois.nic.asia"),
59    ("tel", "whois.nic.tel"),
60    ("cloud", "whois.nic.cloud"),
61    ("global", "whois.nic.global"),
62    ("host", "whois.nic.host"),
63    ("link", "whois.nic.link"),
64    // ccTLDs (Country Codes)
65    ("ac", "whois.nic.ac"),
66    ("ae", "whois.aeda.net.ae"),
67    ("am", "whois.amnic.net"),
68    ("at", "whois.nic.at"),
69    ("au", "whois.auda.org.au"), // covers com.au
70    ("be", "whois.dns.be"),
71    ("br", "whois.registro.br"), // covers com.br
72    ("by", "whois.cctld.by"),
73    ("ca", "whois.cira.ca"),
74    ("ch", "whois.nic.ch"),
75    ("cl", "whois.nic.cl"),
76    ("cn", "whois.cnnic.cn"), // covers com.cn
77    ("co", "whois.nic.co"),
78    ("cz", "whois.nic.cz"),
79    ("de", "whois.denic.de"),
80    ("dk", "whois.dk-hostmaster.dk"),
81    ("dz", "whois.nic.dz"),
82    ("es", "whois.nic.es"),
83    ("eu", "whois.eu"),
84    ("fi", "whois.fi"),
85    ("fr", "whois.nic.fr"),
86    ("hk", "whois.hkirc.hk"),
87    ("hr", "whois.dns.hr"),
88    ("hu", "whois.nic.hu"),
89    ("id", "whois.pandi.or.id"), // covers co.id
90    ("ie", "whois.iedr.ie"),
91    ("il", "whois.isoc.org.il"), // covers co.il
92    ("in", "whois.registry.in"), // covers co.in
93    ("ir", "whois.nic.ir"),
94    ("is", "whois.isnic.is"),
95    ("it", "whois.nic.it"),
96    ("jp", "whois.jprs.jp"), // covers co.jp
97    ("kr", "whois.kr"),      // covers co.kr
98    ("kz", "whois.nic.kz"),
99    ("lt", "whois.domreg.lt"),
100    ("lu", "whois.dns.lu"),
101    ("lv", "whois.nic.lv"),
102    ("ma", "whois.registre.ma"),
103    ("mx", "whois.mx"), // covers com.mx
104    ("nl", "whois.domain-registry.nl"),
105    ("no", "whois.norid.no"),
106    ("nz", "whois.srs.net.nz"), // covers co.nz
107    ("pt", "whois.dns.pt"),
108    ("pl", "whois.dns.pl"), // covers com.pl
109    ("ro", "whois.rotld.ro"),
110    ("rs", "whois.rnids.rs"),
111    ("ru", "whois.tcinet.ru"),
112    ("se", "whois.iis.se"),
113    ("sg", "whois.sgnic.sg"), // covers com.sg
114    ("si", "whois.register.si"),
115    ("sk", "whois.sk-nic.sk"),
116    ("su", "whois.tcinet.ru"),
117    ("th", "whois.thnic.co.th"),   // covers co.th
118    ("tr", "whois.trabis.gov.tr"), // Covers com.tr, org.tr, etc.
119    ("tw", "whois.twnic.net.tw"),  // covers com.tw
120    ("ua", "whois.ua"),            // covers com.ua
121    ("uk", "whois.nic.uk"),        // Covers co.uk, org.uk
122    ("us", "whois.nic.us"),
123    ("za", "whois.registry.net.za"), // covers co.za
124];
125
126/// Common ports for scanning
127const COMMON_PORTS: &[(u16, &str)] = &[
128    (7, "Echo"),
129    (9, "Discard"),
130    (11, "Systat"),
131    (13, "Daytime"),
132    (17, "QOTD"),
133    (19, "Chargen"),
134    (20, "FTP-Data"),
135    (21, "FTP"),
136    (22, "SSH"),
137    (23, "Telnet"),
138    (25, "SMTP"),
139    (26, "RSFTP"),
140    (37, "Time"),
141    (42, "WINS"),
142    (43, "WHOIS"),
143    (49, "TACACS"),
144    (53, "DNS"),
145    (69, "TFTP"),
146    (79, "Finger"),
147    (80, "HTTP"),
148    (81, "HTTP-Alt"),
149    (82, "XFER"),
150    (88, "Kerberos"),
151    (106, "POP3PW"),
152    (110, "POP3"),
153    (111, "RPCBind"),
154    (113, "Ident"),
155    (119, "NNTP"),
156    (135, "MSRPC"),
157    (139, "NetBIOS-SSN"),
158    (143, "IMAP"),
159    (144, "NeWS"),
160    (161, "SNMP"),
161    (179, "BGP"),
162    (199, "SMUX"),
163    (211, "Texas.net"),
164    (212, "ANET"),
165    (222, "RSH-Spam"),
166    (254, "ClearCase"),
167    (255, "BGP"),
168    (256, "RAP"),
169    (259, "ESRO-Gen"),
170    (264, "BGMP"),
171    (280, "HTTP-Mgmt"),
172    (311, "OSX-Server"),
173    (389, "LDAP"),
174    (407, "Timbuktu"),
175    (427, "SLP"),
176    (443, "HTTPS"),
177    (444, "SNPP"),
178    (445, "Microsoft-DS"),
179    (464, "kpasswd"),
180    (465, "SMTPS"),
181    (500, "ISAKMP"),
182    (512, "Exec"),
183    (513, "Login"),
184    (514, "Shell"),
185    (515, "Printer"),
186    (524, "NCP"),
187    (541, "NetWall"),
188    (543, "klogin"),
189    (544, "kshell"),
190    (545, "tk-remote"),
191    (548, "AFP"),
192    (554, "RTSP"),
193    (587, "Submission"),
194    (593, "HTTP-RPC-EPMAP"),
195    (631, "IPP"),
196    (636, "LDAPS"),
197    (646, "LDP"),
198    (749, "Kerberos-Admin"),
199    (808, "CCProxy-HTTP"),
200    (873, "Rsync"),
201    (902, "VMware-Auth"),
202    (989, "FTPS-Data"),
203    (990, "FTPS"),
204    (992, "Telnet-SSL"),
205    (993, "IMAPS"),
206    (995, "POP3S"),
207    (1025, "NFS-or-IIS"),
208    (1026, "LSA"),
209    (1027, "IIS"),
210    (1028, "WinRM"),
211    (1080, "SOCKS"),
212    (1099, "RMI-Registry"),
213    (1194, "OpenVPN"),
214    (1433, "MSSQL"),
215    (1434, "MSSQL-Mgmt"),
216    (1521, "Oracle"),
217    (1524, "Ingres-Lock"),
218    (1720, "H.323"),
219    (1723, "PPTP"),
220    (1883, "MQTT"),
221    (2000, "Cisco-SCCP"),
222    (2049, "NFS"),
223    (2082, "cPanel"),
224    (2083, "cPanel-SSL"),
225    (2086, "WHM"),
226    (2087, "WHM-SSL"),
227    (2095, "Webmail"),
228    (2096, "Webmail-SSL"),
229    (2181, "ZooKeeper"),
230    (2222, "DirectAdmin"),
231    (2375, "Docker"),
232    (2376, "Docker-SSL"),
233    (2601, "Zebra"),
234    (2602, "Rippled"),
235    (2604, "OSPF"),
236    (2605, "BGP"),
237    (3128, "Squid"),
238    (3268, "LDAP-GC"),
239    (3269, "LDAPS-GC"),
240    (3306, "MySQL"),
241    (3389, "RDP"),
242    (3690, "SVN"),
243    (4000, "Diablo"),
244    (4040, "Chef/Subsonic"),
245    (4242, "Rubrics"),
246    (4333, "mSQL"),
247    (4444, "Metasploit-Bind"),
248    (4500, "IPSec-NAT-T"),
249    (4567, "Sinatra"),
250    (4899, "Radmin"),
251    (5000, "UPnP"),
252    (5001, "Iperf"),
253    (5002, "Radio"),
254    (5038, "Asterisk"),
255    (5432, "PostgreSQL"),
256    (5555, "Freeciv"),
257    (5632, "pcAnywhere"),
258    (5672, "AMQP"),
259    (5800, "VNC-HTTP"),
260    (5900, "VNC"),
261    (5901, "VNC-1"),
262    (5938, "TeamViewer"),
263    (5984, "CouchDB"),
264    (6000, "X11"),
265    (6379, "Redis"),
266    (6443, "Kubernetes-API"),
267    (6543, "MythTV"),
268    (6667, "IRC"),
269    (6881, "BitTorrent"),
270    (7000, "Cassandra-Intra"),
271    (7001, "Cassandra-TLS"),
272    (7070, "RealServer"),
273    (7199, "Cassandra-JMX"),
274    (7474, "Neo4j"),
275    (8000, "HTTP-Alt"),
276    (8008, "HTTP-Alt"),
277    (8080, "HTTP-Proxy"),
278    (8081, "HTTP-Proxy"),
279    (8090, "Atlassian-Confluence"),
280    (8443, "HTTPS-Alt"),
281    (8883, "MQTT-SSL"),
282    (8888, "HTTP-Alt"),
283    (9000, "SonarQube/Portainer"),
284    (9042, "Cassandra-CQL"),
285    (9090, "Prometheus"),
286    (9092, "Kafka"),
287    (9100, "JetDirect/PromExporter"),
288    (9160, "Cassandra-Thrift"),
289    (9200, "Elasticsearch"),
290    (9300, "Elasticsearch-Node"),
291    (9443, "Portainer-SSL"),
292    (10000, "Webmin"),
293    (10001, "Webmin-Alt"),
294    (10250, "Kubelet-API"),
295    (11211, "Memcached"),
296    (27017, "MongoDB"),
297    (27018, "MongoDB-Shard"),
298    (27019, "MongoDB-Config"),
299    (28017, "MongoDB-Web"),
300    (50000, "SAP/DB2"),
301    (50070, "Hadoop-Namenode"),
302    (61616, "ActiveMQ"),
303    (8086, "InfluxDB"),
304    (8181, "GlassFish"),
305    (17500, "Dropbox"),
306    (25565, "Minecraft"),
307    (27015, "HLDS/Steam"),
308    (30000, "K8s-NodePort"),
309];
310
311/// Security headers to check
312const SECURITY_HEADERS: &[&str] = &[
313    "strict-transport-security",
314    "x-frame-options",
315    "x-content-type-options",
316    "x-xss-protection",
317    "content-security-policy",
318    "content-security-policy-report-only",
319    "permissions-policy",
320    "referrer-policy",
321    "x-permitted-cross-domain-policies",
322    "expect-ct",
323    "cross-origin-embedder-policy",
324    "cross-origin-opener-policy",
325    "cross-origin-resource-policy",
326    "access-control-allow-origin",
327    "server-timing",
328];
329
330/// Privacy keywords in WHOIS output
331const PRIVACY_KEYWORDS: &[&str] = &[
332    "redacted",
333    "privacy",
334    "gdpr",
335    "protected",
336    "proxy",
337    "private",
338];
339
340// ── Data Structures ─────────────────────────────────────────────────────────
341
342#[derive(Debug, Clone, Serialize, Deserialize)]
343pub struct DomainInfoResult {
344    pub domain: String,
345    pub ipv4: Option<String>,
346    pub ipv6: Vec<String>,
347    pub all_ipv4: Vec<String>,
348    pub reverse_dns: Option<String>,
349    pub whois: WhoisInfo,
350    pub ssl: SslInfo,
351    pub dns: DnsInfo,
352    pub open_ports: Vec<String>,
353    pub http_status: Option<String>,
354    pub web_server: Option<String>,
355    pub response_time_ms: Option<f64>,
356    pub security: SecurityInfo,
357    pub security_score: u32,
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct WhoisInfo {
362    pub registrar: String,
363    pub creation_date: String,
364    pub expiry_date: String,
365    pub last_updated: String,
366    pub domain_status: Vec<String>,
367    pub registrant: String,
368    pub privacy_protection: String,
369    #[serde(skip_serializing_if = "Vec::is_empty")]
370    pub name_servers: Vec<String>,
371}
372
373#[derive(Debug, Clone, Serialize, Deserialize)]
374pub struct SslInfo {
375    pub status: String,
376    #[serde(skip_serializing_if = "Option::is_none")]
377    pub issued_to: Option<String>,
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub issuer: Option<String>,
380    #[serde(skip_serializing_if = "Option::is_none")]
381    pub protocol_version: Option<String>,
382    #[serde(skip_serializing_if = "Option::is_none")]
383    pub expiry_date: Option<String>,
384    #[serde(skip_serializing_if = "Option::is_none")]
385    pub days_until_expiry: Option<i64>,
386    #[serde(skip_serializing_if = "Vec::is_empty")]
387    pub alternative_names: Vec<String>,
388}
389
390#[derive(Debug, Clone, Serialize, Deserialize)]
391pub struct DnsInfo {
392    pub nameservers: Vec<String>,
393    pub mx_records: Vec<String>,
394    pub txt_records: Vec<String>,
395    #[serde(skip_serializing_if = "Option::is_none")]
396    pub spf: Option<String>,
397    #[serde(skip_serializing_if = "Option::is_none")]
398    pub dmarc: Option<String>,
399}
400
401#[derive(Debug, Clone, Serialize, Deserialize)]
402pub struct SecurityInfo {
403    pub https_available: bool,
404    pub https_redirect: bool,
405    pub security_headers: HashMap<String, String>,
406    pub headers_count: usize,
407}
408
409// ── Main function ───────────────────────────────────────────────────────────
410
411pub async fn get_domain_info(
412    domain: &str,
413    progress_tx: Option<tokio::sync::mpsc::Sender<crate::ScanProgress>>,
414) -> Result<DomainInfoResult, Box<dyn std::error::Error + Send + Sync>> {
415    let clean = clean_domain(domain);
416
417    let client = crate::http_client_builder()
418        .timeout(Duration::from_secs(5))
419        .danger_accept_invalid_certs(true)
420        .redirect(reqwest::redirect::Policy::limited(3))
421        .user_agent("Mozilla/5.0")
422        .build()?;
423
424    if let Some(t) = &progress_tx {
425        let _ = t
426            .send(crate::ScanProgress {
427                module: "Domain Info".into(),
428                percentage: 5.0,
429                message: format!("Initializing scan for {}", clean),
430                status: "Info".into(),
431            })
432            .await;
433    }
434
435    // ── IP Resolution ───────────────────────────────────────────────────
436    let (mut ipv4, mut all_ipv4, mut ipv6) = (None, vec![], vec![]);
437    if let Some(t) = &progress_tx {
438        let _ = t
439            .send(crate::ScanProgress {
440                module: "IP Resolution".into(),
441                percentage: 10.0,
442                message: "Resolving IP addresses...".into(),
443                status: "Info".into(),
444            })
445            .await;
446    }
447
448    if let Ok(addrs) = tokio::net::lookup_host(format!("{}:80", clean)).await {
449        for addr in addrs {
450            match addr.ip() {
451                std::net::IpAddr::V4(ip) => {
452                    all_ipv4.push(ip.to_string());
453                }
454                std::net::IpAddr::V6(ip) => {
455                    ipv6.push(ip.to_string());
456                }
457            }
458        }
459    }
460    if !all_ipv4.is_empty() {
461        ipv4 = Some(all_ipv4[0].clone());
462    }
463    if let Some(t) = &progress_tx {
464        let _ = t
465            .send(crate::ScanProgress {
466                module: "IP Resolution".into(),
467                percentage: 15.0,
468                message: "IP Resolution completed".into(),
469                status: "Success".into(),
470            })
471            .await;
472    }
473
474    // ── Reverse DNS ─────────────────────────────────────────────────────
475    if let Some(t) = &progress_tx {
476        let _ = t
477            .send(crate::ScanProgress {
478                module: "Reverse DNS".into(),
479                percentage: 18.0,
480                message: "Looking up reverse DNS...".into(),
481                status: "Info".into(),
482            })
483            .await;
484    }
485    let reverse_dns = if let Some(ref ip) = ipv4 {
486        reverse_dns_lookup(ip).await
487    } else {
488        None
489    };
490    if let Some(t) = &progress_tx {
491        let _ = t
492            .send(crate::ScanProgress {
493                module: "Reverse DNS".into(),
494                percentage: 20.0,
495                message: "Reverse DNS completed".into(),
496                status: "Success".into(),
497            })
498            .await;
499    }
500
501    // ── Run concurrent tasks ────────────────────────────────────────────
502    let whois_fut = async {
503        if let Some(t) = &progress_tx {
504            let _ = t
505                .send(crate::ScanProgress {
506                    module: "WHOIS".into(),
507                    percentage: 25.0,
508                    message: "Querying WHOIS registries...".into(),
509                    status: "Info".into(),
510                })
511                .await;
512        }
513        let res = query_whois(&clean).await;
514        if let Some(t) = &progress_tx {
515            let _ = t
516                .send(crate::ScanProgress {
517                    module: "WHOIS".into(),
518                    percentage: 40.0,
519                    message: "WHOIS data retrieved".into(),
520                    status: "Success".into(),
521                })
522                .await;
523        }
524        res
525    };
526    let ssl_fut = async {
527        if let Some(t) = &progress_tx {
528            let _ = t
529                .send(crate::ScanProgress {
530                    module: "SSL".into(),
531                    percentage: 30.0,
532                    message: "Verifying SSL certificates...".into(),
533                    status: "Info".into(),
534                })
535                .await;
536        }
537        let res = check_ssl(&clean).await;
538        if let Some(t) = &progress_tx {
539            let _ = t
540                .send(crate::ScanProgress {
541                    module: "SSL".into(),
542                    percentage: 50.0,
543                    message: "SSL certificate validated".into(),
544                    status: "Success".into(),
545                })
546                .await;
547        }
548        res
549    };
550    let dns_fut = async {
551        if let Some(t) = &progress_tx {
552            let _ = t
553                .send(crate::ScanProgress {
554                    module: "DNS".into(),
555                    percentage: 35.0,
556                    message: "Fetching DNS records...".into(),
557                    status: "Info".into(),
558                })
559                .await;
560        }
561        let res = get_dns_records(&clean).await;
562        if let Some(t) = &progress_tx {
563            let _ = t
564                .send(crate::ScanProgress {
565                    module: "DNS".into(),
566                    percentage: 60.0,
567                    message: "DNS records retrieved".into(),
568                    status: "Success".into(),
569                })
570                .await;
571        }
572        res
573    };
574    let ports_fut = async {
575        if let Some(t) = &progress_tx {
576            let _ = t
577                .send(crate::ScanProgress {
578                    module: "Ports".into(),
579                    percentage: 40.0,
580                    message: "Scanning common ports...".into(),
581                    status: "Info".into(),
582                })
583                .await;
584        }
585        let res = scan_ports(ipv4.as_deref()).await;
586        if let Some(t) = &progress_tx {
587            let _ = t
588                .send(crate::ScanProgress {
589                    module: "Ports".into(),
590                    percentage: 70.0,
591                    message: "Port scanning complete".into(),
592                    status: "Success".into(),
593                })
594                .await;
595        }
596        res
597    };
598    let http_fut = async {
599        if let Some(t) = &progress_tx {
600            let _ = t
601                .send(crate::ScanProgress {
602                    module: "HTTP".into(),
603                    percentage: 45.0,
604                    message: "Checking HTTP status...".into(),
605                    status: "Info".into(),
606                })
607                .await;
608        }
609        let res = check_http_status(&client, &clean).await;
610        if let Some(t) = &progress_tx {
611            let _ = t
612                .send(crate::ScanProgress {
613                    module: "HTTP".into(),
614                    percentage: 80.0,
615                    message: "HTTP check complete".into(),
616                    status: "Success".into(),
617                })
618                .await;
619        }
620        res
621    };
622    let security_fut = async {
623        if let Some(t) = &progress_tx {
624            let _ = t
625                .send(crate::ScanProgress {
626                    module: "Security".into(),
627                    percentage: 50.0,
628                    message: "Analyzing security headers...".into(),
629                    status: "Info".into(),
630                })
631                .await;
632        }
633        let res = check_security(&client, &clean).await;
634        if let Some(t) = &progress_tx {
635            let _ = t
636                .send(crate::ScanProgress {
637                    module: "Security".into(),
638                    percentage: 90.0,
639                    message: "Security analysis complete".into(),
640                    status: "Success".into(),
641                })
642                .await;
643        }
644        res
645    };
646
647    let (whois, ssl, dns, open_ports, http_info, security) = tokio::join!(
648        whois_fut,
649        ssl_fut,
650        dns_fut,
651        ports_fut,
652        http_fut,
653        security_fut
654    );
655
656    // ── Security Score ──────────────────────────────────────────────────
657    let score = calculate_security_score(&ssl, &dns, &security);
658
659    Ok(DomainInfoResult {
660        domain: clean,
661        ipv4,
662        ipv6,
663        all_ipv4,
664        reverse_dns,
665        whois,
666        ssl,
667        dns,
668        open_ports,
669        http_status: http_info.0,
670        web_server: http_info.1,
671        response_time_ms: http_info.2,
672        security,
673        security_score: score,
674    })
675}
676
677// ── Domain cleaning ─────────────────────────────────────────────────────────
678
679fn clean_domain(domain: &str) -> String {
680    let d = domain
681        .trim_start_matches("https://")
682        .trim_start_matches("http://")
683        .replace("www.", "");
684    d.split('/')
685        .next()
686        .unwrap_or(&d)
687        .split(':')
688        .next()
689        .unwrap_or(&d)
690        .to_string()
691}
692
693// ── Reverse DNS ─────────────────────────────────────────────────────────────
694
695pub async fn reverse_dns_lookup(ip: &str) -> Option<String> {
696    let output = tokio::process::Command::new("dig")
697        .args(["+short", "-x", ip])
698        .output()
699        .await
700        .ok()?;
701    let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
702    if text.is_empty() {
703        None
704    } else {
705        Some(text.trim_end_matches('.').to_string())
706    }
707}
708
709// ── WHOIS via TCP socket ────────────────────────────────────────────────────
710
711fn get_whois_server(domain: &str) -> &'static str {
712    let tld = domain.split('.').next_back().unwrap_or("");
713    WHOIS_SERVERS
714        .iter()
715        .find(|(t, _)| *t == tld)
716        .map(|(_, s)| *s)
717        .unwrap_or("whois.iana.org")
718}
719
720async fn query_whois_tcp(domain: &str, server: &str) -> Option<String> {
721    let addr = format!("{}:43", server);
722    let mut stream = tokio::time::timeout(Duration::from_secs(10), TcpStream::connect(&addr))
723        .await
724        .ok()?
725        .ok()?;
726
727    stream
728        .write_all(format!("{}\r\n", domain).as_bytes())
729        .await
730        .ok()?;
731
732    let mut buf = Vec::new();
733    let _ = tokio::time::timeout(Duration::from_secs(10), stream.read_to_end(&mut buf)).await;
734
735    Some(String::from_utf8_lossy(&buf).to_string())
736}
737
738pub async fn query_whois(domain: &str) -> WhoisInfo {
739    let mut info = WhoisInfo {
740        registrar: "Unknown".into(),
741        creation_date: "Unknown".into(),
742        expiry_date: "Unknown".into(),
743        last_updated: "Unknown".into(),
744        domain_status: vec![],
745        registrant: "Unknown".into(),
746        privacy_protection: "Unknown".into(),
747        name_servers: vec![],
748    };
749
750    let server = get_whois_server(domain);
751    let output = match query_whois_tcp(domain, server).await {
752        Some(o) if !o.is_empty() => o,
753        _ => return info,
754    };
755
756    // Follow referral
757    let final_output = if let Some(caps) = Regex::new(r"(?i)Registrar WHOIS Server:\s*(.+)")
758        .ok()
759        .and_then(|r| r.captures(&output))
760    {
761        let referral = caps
762            .get(1)
763            .unwrap()
764            .as_str()
765            .trim()
766            .replace("whois://", "")
767            .replace("http://", "")
768            .replace("https://", "");
769
770        if let Some(ref_out) = query_whois_tcp(domain, &referral).await {
771            format!("{}\n---\n{}", output, ref_out)
772        } else {
773            output
774        }
775    } else {
776        output
777    };
778
779    // Parse registrar
780    for pat in &[
781        r"(?i)Registrar:\s*(.+)",
782        r"(?i)Registrar Name:\s*(.+)",
783        r"(?i)Registrar Organization:\s*(.+)",
784    ] {
785        if let Some(m) = Regex::new(pat).ok().and_then(|r| r.captures(&final_output)) {
786            info.registrar = m.get(1).unwrap().as_str().trim().to_string();
787            break;
788        }
789    }
790
791    // Parse creation date
792    for pat in &[
793        r"(?i)Creation Date:\s*(.+)",
794        r"(?i)Created Date:\s*(.+)",
795        r"(?i)Created:\s*(.+)",
796        r"(?i)Registration Time:\s*(.+)",
797    ] {
798        if let Some(m) = Regex::new(pat).ok().and_then(|r| r.captures(&final_output)) {
799            info.creation_date = m
800                .get(1)
801                .unwrap()
802                .as_str()
803                .trim()
804                .split('\n')
805                .next()
806                .unwrap_or("")
807                .to_string();
808            break;
809        }
810    }
811
812    // Parse expiry date
813    for pat in &[
814        r"(?i)Registry Expiry Date:\s*(.+)",
815        r"(?i)Registrar Registration Expiration Date:\s*(.+)",
816        r"(?i)Expir(?:y|ation) Date:\s*(.+)",
817        r"(?i)expires:\s*(.+)",
818        r"(?i)Expiration Time:\s*(.+)",
819    ] {
820        if let Some(m) = Regex::new(pat).ok().and_then(|r| r.captures(&final_output)) {
821            info.expiry_date = m
822                .get(1)
823                .unwrap()
824                .as_str()
825                .trim()
826                .split('\n')
827                .next()
828                .unwrap_or("")
829                .to_string();
830            break;
831        }
832    }
833
834    // Parse updated date
835    for pat in &[
836        r"(?i)Updated Date:\s*(.+)",
837        r"(?i)Last Updated:\s*(.+)",
838        r"(?i)last-update:\s*(.+)",
839        r"(?i)Modified Date:\s*(.+)",
840    ] {
841        if let Some(m) = Regex::new(pat).ok().and_then(|r| r.captures(&final_output)) {
842            info.last_updated = m
843                .get(1)
844                .unwrap()
845                .as_str()
846                .trim()
847                .split('\n')
848                .next()
849                .unwrap_or("")
850                .to_string();
851            break;
852        }
853    }
854
855    // Parse domain status
856    if let Ok(rx) = Regex::new(r"(?i)(?:Domain )?Status:\s*(.+)") {
857        info.domain_status = rx
858            .captures_iter(&final_output)
859            .filter_map(|c| {
860                c.get(1).map(|m| {
861                    m.as_str()
862                        .split_whitespace()
863                        .next()
864                        .unwrap_or("")
865                        .to_string()
866                })
867            })
868            .filter(|s| !s.is_empty())
869            .take(3)
870            .collect();
871    }
872    if info.domain_status.is_empty() {
873        info.domain_status.push("Unknown".into());
874    }
875
876    // Parse registrant
877    for pat in &[
878        r"(?i)Registrant Name:\s*(.+)",
879        r"(?i)Registrant:\s*(.+)",
880        r"(?i)Registrant Organization:\s*(.+)",
881    ] {
882        if let Some(m) = Regex::new(pat).ok().and_then(|r| r.captures(&final_output)) {
883            let val = m
884                .get(1)
885                .unwrap()
886                .as_str()
887                .trim()
888                .split('\n')
889                .next()
890                .unwrap_or("")
891                .to_string();
892            if !val.is_empty() {
893                info.registrant = val;
894                break;
895            }
896        }
897    }
898
899    // Privacy protection
900    let lower = final_output.to_lowercase();
901    info.privacy_protection = if PRIVACY_KEYWORDS.iter().any(|k| lower.contains(k)) {
902        "Active".into()
903    } else {
904        "Inactive".into()
905    };
906
907    // Name servers
908    if let Ok(rx) = Regex::new(r"(?i)Name Server:\s*(.+)") {
909        info.name_servers = rx
910            .captures_iter(&final_output)
911            .filter_map(|c| c.get(1).map(|m| m.as_str().trim().to_lowercase()))
912            .take(4)
913            .collect();
914    }
915
916    info
917}
918
919// ── SSL Certificate ─────────────────────────────────────────────────────────
920
921pub async fn check_ssl(domain: &str) -> SslInfo {
922    // Use openssl s_client to get certificate info
923    let output = match tokio::process::Command::new("openssl")
924        .args([
925            "s_client",
926            "-connect",
927            &format!("{}:443", domain),
928            "-servername",
929            domain,
930        ])
931        .stdin(std::process::Stdio::null())
932        .stdout(std::process::Stdio::piped())
933        .stderr(std::process::Stdio::piped())
934        .output()
935        .await
936    {
937        Ok(o) => String::from_utf8_lossy(&o.stdout).to_string(),
938        Err(_) => {
939            return SslInfo {
940                status: "Error".into(),
941                issued_to: None,
942                issuer: None,
943                protocol_version: None,
944                expiry_date: None,
945                days_until_expiry: None,
946                alternative_names: vec![],
947            }
948        }
949    };
950
951    if output.contains("CONNECTED") {
952        let mut ssl = SslInfo {
953            status: "Valid".into(),
954            issued_to: None,
955            issuer: None,
956            protocol_version: None,
957            expiry_date: None,
958            days_until_expiry: None,
959            alternative_names: vec![],
960        };
961
962        // Extract subject CN
963        if let Some(m) = Regex::new(r"subject=.*?CN\s*=\s*([^\n/,]+)")
964            .ok()
965            .and_then(|r| r.captures(&output))
966        {
967            ssl.issued_to = Some(m.get(1).unwrap().as_str().trim().to_string());
968        }
969
970        // Extract issuer CN
971        if let Some(m) = Regex::new(r"issuer=.*?CN\s*=\s*([^\n/,]+)")
972            .ok()
973            .and_then(|r| r.captures(&output))
974        {
975            ssl.issuer = Some(m.get(1).unwrap().as_str().trim().to_string());
976        }
977
978        // Extract protocol
979        if let Some(m) = Regex::new(r"Protocol\s*:\s*(.+)")
980            .ok()
981            .and_then(|r| r.captures(&output))
982        {
983            ssl.protocol_version = Some(m.get(1).unwrap().as_str().trim().to_string());
984        }
985
986        // Get dates via openssl x509
987        if let Ok(cert_output) = tokio::process::Command::new("sh")
988            .args(["-c", &format!("echo | openssl s_client -connect {}:443 -servername {} 2>/dev/null | openssl x509 -noout -dates -subject -ext subjectAltName 2>/dev/null", domain, domain)])
989            .output()
990            .await
991        {
992            let cert_text = String::from_utf8_lossy(&cert_output.stdout);
993
994            if let Some(m) = Regex::new(r"notAfter=(.+)").ok().and_then(|r| r.captures(&cert_text)) {
995                let expiry_str = m.get(1).unwrap().as_str().trim().to_string();
996                ssl.expiry_date = Some(expiry_str.clone());
997
998                // Compute days_until_expiry from parsed date
999                // OpenSSL format: "Jun 15 12:00:00 2025 GMT" or "Jun  5 12:00:00 2025 GMT"
1000                let clean_expiry = expiry_str.trim_end_matches(" GMT").trim_end_matches(" UTC");
1001
1002                // Try parsing with space-padded day (%e) or zero-padded day (%d)
1003                let parsed_date = chrono::NaiveDateTime::parse_from_str(clean_expiry, "%b %e %H:%M:%S %Y")
1004                    .or_else(|_| chrono::NaiveDateTime::parse_from_str(clean_expiry, "%b %d %H:%M:%S %Y"));
1005
1006                if let Ok(expiry) = parsed_date {
1007                    let now = chrono::Utc::now().naive_utc();
1008                    ssl.days_until_expiry = Some((expiry - now).num_days());
1009                }
1010            }
1011
1012            // Extract SANs
1013            if let Some(san_section) = cert_text.split("X509v3 Subject Alternative Name:").nth(1) {
1014                let names: Vec<String> = Regex::new(r"DNS:([^,\s]+)")
1015                    .ok()
1016                    .map(|r| r.captures_iter(san_section).filter_map(|c| c.get(1).map(|m| m.as_str().to_string())).take(5).collect())
1017                    .unwrap_or_default();
1018                ssl.alternative_names = names;
1019            }
1020        }
1021
1022        ssl
1023    } else {
1024        SslInfo {
1025            status: "HTTPS not available".into(),
1026            issued_to: None,
1027            issuer: None,
1028            protocol_version: None,
1029            expiry_date: None,
1030            days_until_expiry: None,
1031            alternative_names: vec![],
1032        }
1033    }
1034}
1035
1036// ── DNS Records via dig ─────────────────────────────────────────────────────
1037
1038async fn dig_query(domain: &str, rtype: &str) -> Vec<String> {
1039    tokio::process::Command::new("dig")
1040        .args(["+short", rtype, domain])
1041        .output()
1042        .await
1043        .ok()
1044        .and_then(|o| String::from_utf8(o.stdout).ok())
1045        .map(|t| {
1046            t.lines()
1047                .filter(|l| !l.trim().is_empty() && !l.starts_with(';'))
1048                .map(|l| l.trim().to_string())
1049                .collect()
1050        })
1051        .unwrap_or_default()
1052}
1053
1054pub async fn get_dns_records(domain: &str) -> DnsInfo {
1055    let (ns, mx, txt) = tokio::join!(
1056        dig_query(domain, "NS"),
1057        dig_query(domain, "MX"),
1058        dig_query(domain, "TXT"),
1059    );
1060
1061    let spf = txt.iter().find(|t| t.contains("v=spf1")).cloned();
1062    let dmarc_records = dig_query(&format!("_dmarc.{}", domain), "TXT").await;
1063    let dmarc = dmarc_records.into_iter().find(|t| t.contains("v=DMARC1"));
1064
1065    DnsInfo {
1066        nameservers: ns,
1067        mx_records: mx,
1068        txt_records: txt,
1069        spf,
1070        dmarc,
1071    }
1072}
1073
1074// ── Port Scanning ───────────────────────────────────────────────────────────
1075
1076pub async fn scan_ports(ip: Option<&str>) -> Vec<String> {
1077    let ip = match ip {
1078        Some(ip) => ip,
1079        None => return vec![],
1080    };
1081
1082    let mut results = Vec::new();
1083    let mut handles = Vec::new();
1084
1085    for &(port, service) in COMMON_PORTS {
1086        let addr = format!("{}:{}", ip, port);
1087        handles.push(tokio::spawn(async move {
1088            match tokio::time::timeout(Duration::from_secs(1), TcpStream::connect(&addr)).await {
1089                Ok(Ok(_)) => Some(format!("{}/{}", port, service)),
1090                _ => None,
1091            }
1092        }));
1093    }
1094
1095    for handle in handles {
1096        if let Ok(Some(port_str)) = handle.await {
1097            results.push(port_str);
1098        }
1099    }
1100
1101    results.sort();
1102    results
1103}
1104
1105// ── HTTP Status Check ───────────────────────────────────────────────────────
1106
1107pub async fn check_http_status(
1108    client: &Client,
1109    domain: &str,
1110) -> (Option<String>, Option<String>, Option<f64>) {
1111    for proto in &["https", "http"] {
1112        let url = format!("{}://{}", proto, domain);
1113        let start = Instant::now();
1114        match client.get(&url).send().await {
1115            Ok(resp) => {
1116                let elapsed = start.elapsed().as_secs_f64() * 1000.0;
1117                let status_str = format!("{} - {}", resp.status().as_u16(), proto.to_uppercase());
1118                let server = resp
1119                    .headers()
1120                    .get("server")
1121                    .and_then(|v| v.to_str().ok())
1122                    .map(|s| s.to_string());
1123                return (
1124                    Some(status_str),
1125                    server,
1126                    Some((elapsed * 100.0).round() / 100.0),
1127                );
1128            }
1129            Err(_) => continue,
1130        }
1131    }
1132    (None, None, None)
1133}
1134
1135// ── Security Check ──────────────────────────────────────────────────────────
1136
1137pub async fn check_security(client: &Client, domain: &str) -> SecurityInfo {
1138    let mut sec = SecurityInfo {
1139        https_available: false,
1140        https_redirect: false,
1141        security_headers: HashMap::new(),
1142        headers_count: 0,
1143    };
1144
1145    // HTTPS + security headers
1146    if let Ok(resp) = client.get(format!("https://{}", domain)).send().await {
1147        sec.https_available = true;
1148        for header in SECURITY_HEADERS {
1149            if let Some(val) = resp.headers().get(*header) {
1150                if let Ok(v) = val.to_str() {
1151                    sec.security_headers
1152                        .insert(header.to_string(), v.to_string());
1153                    sec.headers_count += 1;
1154                }
1155            }
1156        }
1157    }
1158
1159    // HTTP → HTTPS redirect
1160    if let Ok(resp) = client.get(format!("http://{}", domain)).send().await {
1161        let final_url = resp.url().to_string();
1162        if final_url.starts_with("https://") {
1163            sec.https_redirect = true;
1164        }
1165    }
1166
1167    sec
1168}
1169
1170// ── Security Score (0-100) ──────────────────────────────────────────────────
1171
1172pub fn calculate_security_score(ssl: &SslInfo, dns: &DnsInfo, security: &SecurityInfo) -> u32 {
1173    let mut score: u32 = 0;
1174
1175    // HTTPS available (+30)
1176    if security.https_available {
1177        score += 30;
1178    }
1179
1180    // HTTPS redirect (+10)
1181    if security.https_redirect {
1182        score += 10;
1183    }
1184
1185    // SSL valid (+20)
1186    if ssl.status == "Valid" {
1187        score += 20;
1188    }
1189
1190    // Security headers (up to +20, 4 points each)
1191    score += (security.headers_count as u32 * 4).min(20);
1192
1193    // SPF record (+10)
1194    if dns.spf.is_some() {
1195        score += 10;
1196    }
1197
1198    // DMARC record (+10)
1199    if dns.dmarc.is_some() {
1200        score += 10;
1201    }
1202
1203    score
1204}