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
9const WHOIS_SERVERS: &[(&str, &str)] = &[
12 ("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 ("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 ("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"), ("be", "whois.dns.be"),
73 ("br", "whois.registro.br"), ("by", "whois.cctld.by"),
75 ("ca", "whois.cira.ca"),
76 ("ch", "whois.nic.ch"),
77 ("cl", "whois.nic.cl"),
78 ("cn", "whois.cnnic.cn"), ("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"), ("ie", "whois.iedr.ie"),
93 ("il", "whois.isoc.org.il"), ("in", "whois.registry.in"), ("ir", "whois.nic.ir"),
96 ("is", "whois.isnic.is"),
97 ("it", "whois.nic.it"),
98 ("jp", "whois.jprs.jp"), ("kr", "whois.kr"), ("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"), ("nl", "whois.domain-registry.nl"),
107 ("no", "whois.norid.no"),
108 ("nz", "whois.srs.net.nz"), ("pt", "whois.dns.pt"),
110 ("pl", "whois.dns.pl"), ("ro", "whois.rotld.ro"),
112 ("rs", "whois.rnids.rs"),
113 ("ru", "whois.tcinet.ru"),
114 ("se", "whois.iis.se"),
115 ("sg", "whois.sgnic.sg"), ("si", "whois.register.si"),
117 ("sk", "whois.sk-nic.sk"),
118 ("su", "whois.tcinet.ru"),
119 ("th", "whois.thnic.co.th"), ("tr", "whois.trabis.gov.tr"), ("tw", "whois.twnic.net.tw"), ("ua", "whois.ua"), ("uk", "whois.nic.uk"), ("us", "whois.nic.us"),
125 ("za", "whois.registry.net.za"), ];
127
128const 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
173const 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
192const PRIVACY_KEYWORDS: &[&str] = &[
194 "redacted",
195 "privacy",
196 "gdpr",
197 "protected",
198 "proxy",
199 "private",
200];
201
202#[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
271pub 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 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 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 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 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
386fn 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
402pub 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
418fn 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 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 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 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 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 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 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 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 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 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
628pub async fn check_ssl(domain: &str) -> SslInfo {
631 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 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 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 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 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 let clean_expiry = expiry_str.trim_end_matches(" GMT").trim_end_matches(" UTC");
710
711 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 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
745async 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
783pub 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
814pub 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
844pub 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 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 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
879pub fn calculate_security_score(ssl: &SslInfo, dns: &DnsInfo, security: &SecurityInfo) -> u32 {
882 let mut score: u32 = 0;
883
884 if security.https_available {
886 score += 30;
887 }
888
889 if security.https_redirect {
891 score += 10;
892 }
893
894 if ssl.status == "Valid" {
896 score += 20;
897 }
898
899 score += (security.headers_count as u32 * 4).min(20);
901
902 if dns.spf.is_some() {
904 score += 10;
905 }
906
907 if dns.dmarc.is_some() {
909 score += 10;
910 }
911
912 score
913}