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