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 ("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 ("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"), ("be", "whois.dns.be"),
71 ("br", "whois.registro.br"), ("by", "whois.cctld.by"),
73 ("ca", "whois.cira.ca"),
74 ("ch", "whois.nic.ch"),
75 ("cl", "whois.nic.cl"),
76 ("cn", "whois.cnnic.cn"), ("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"), ("ie", "whois.iedr.ie"),
91 ("il", "whois.isoc.org.il"), ("in", "whois.registry.in"), ("ir", "whois.nic.ir"),
94 ("is", "whois.isnic.is"),
95 ("it", "whois.nic.it"),
96 ("jp", "whois.jprs.jp"), ("kr", "whois.kr"), ("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"), ("nl", "whois.domain-registry.nl"),
105 ("no", "whois.norid.no"),
106 ("nz", "whois.srs.net.nz"), ("pt", "whois.dns.pt"),
108 ("pl", "whois.dns.pl"), ("ro", "whois.rotld.ro"),
110 ("rs", "whois.rnids.rs"),
111 ("ru", "whois.tcinet.ru"),
112 ("se", "whois.iis.se"),
113 ("sg", "whois.sgnic.sg"), ("si", "whois.register.si"),
115 ("sk", "whois.sk-nic.sk"),
116 ("su", "whois.tcinet.ru"),
117 ("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"),
123 ("za", "whois.registry.net.za"), ];
125
126const 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
311const 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
330const PRIVACY_KEYWORDS: &[&str] = &[
332 "redacted",
333 "privacy",
334 "gdpr",
335 "protected",
336 "proxy",
337 "private",
338];
339
340#[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
409pub 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 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 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 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 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
677fn 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
693pub 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
709fn 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 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 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 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 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 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 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 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 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 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
919pub async fn check_ssl(domain: &str) -> SslInfo {
922 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 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 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 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 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 let clean_expiry = expiry_str.trim_end_matches(" GMT").trim_end_matches(" UTC");
1001
1002 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 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
1036async 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
1074pub 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
1105pub 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
1135pub 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 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 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
1170pub fn calculate_security_score(ssl: &SslInfo, dns: &DnsInfo, security: &SecurityInfo) -> u32 {
1173 let mut score: u32 = 0;
1174
1175 if security.https_available {
1177 score += 30;
1178 }
1179
1180 if security.https_redirect {
1182 score += 10;
1183 }
1184
1185 if ssl.status == "Valid" {
1187 score += 20;
1188 }
1189
1190 score += (security.headers_count as u32 * 4).min(20);
1192
1193 if dns.spf.is_some() {
1195 score += 10;
1196 }
1197
1198 if dns.dmarc.is_some() {
1200 score += 10;
1201 }
1202
1203 score
1204}