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"),
13 ("net", "whois.verisign-grs.com"),
14 ("org", "whois.pir.org"),
15 ("info", "whois.afilias.net"),
16 ("biz", "whois.biz"),
17 ("us", "whois.nic.us"),
18 ("uk", "whois.nic.uk"),
19 ("de", "whois.denic.de"),
20 ("fr", "whois.nic.fr"),
21 ("it", "whois.nic.it"),
22 ("nl", "whois.domain-registry.nl"),
23 ("eu", "whois.eu"),
24 ("ru", "whois.tcinet.ru"),
25 ("cn", "whois.cnnic.cn"),
26 ("jp", "whois.jprs.jp"),
27 ("br", "whois.registro.br"),
28 ("au", "whois.auda.org.au"),
29 ("ca", "whois.cira.ca"),
30 ("in", "whois.registry.in"),
31 ("tr", "whois.nic.tr"),
32 ("co", "whois.nic.co"),
33 ("io", "whois.nic.io"),
34 ("me", "whois.nic.me"),
35 ("tv", "whois.nic.tv"),
36 ("cc", "whois.nic.cc"),
37];
38
39const COMMON_PORTS: &[(u16, &str)] = &[
41 (21, "FTP"),
42 (22, "SSH"),
43 (25, "SMTP"),
44 (80, "HTTP"),
45 (443, "HTTPS"),
46 (3306, "MySQL"),
47 (5432, "PostgreSQL"),
48 (8080, "HTTP-Alt"),
49 (8443, "HTTPS-Alt"),
50];
51
52const SECURITY_HEADERS: &[&str] = &[
54 "strict-transport-security",
55 "x-frame-options",
56 "x-content-type-options",
57 "x-xss-protection",
58 "content-security-policy",
59];
60
61const PRIVACY_KEYWORDS: &[&str] = &[
63 "redacted",
64 "privacy",
65 "gdpr",
66 "protected",
67 "proxy",
68 "private",
69];
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct DomainInfoResult {
75 pub domain: String,
76 pub ipv4: Option<String>,
77 pub ipv6: Vec<String>,
78 pub all_ipv4: Vec<String>,
79 pub reverse_dns: Option<String>,
80 pub whois: WhoisInfo,
81 pub ssl: SslInfo,
82 pub dns: DnsInfo,
83 pub open_ports: Vec<String>,
84 pub http_status: Option<String>,
85 pub web_server: Option<String>,
86 pub response_time_ms: Option<f64>,
87 pub security: SecurityInfo,
88 pub security_score: u32,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct WhoisInfo {
93 pub registrar: String,
94 pub creation_date: String,
95 pub expiry_date: String,
96 pub last_updated: String,
97 pub domain_status: Vec<String>,
98 pub registrant: String,
99 pub privacy_protection: String,
100 #[serde(skip_serializing_if = "Vec::is_empty")]
101 pub name_servers: Vec<String>,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct SslInfo {
106 pub status: String,
107 #[serde(skip_serializing_if = "Option::is_none")]
108 pub issued_to: Option<String>,
109 #[serde(skip_serializing_if = "Option::is_none")]
110 pub issuer: Option<String>,
111 #[serde(skip_serializing_if = "Option::is_none")]
112 pub protocol_version: Option<String>,
113 #[serde(skip_serializing_if = "Option::is_none")]
114 pub expiry_date: Option<String>,
115 #[serde(skip_serializing_if = "Option::is_none")]
116 pub days_until_expiry: Option<i64>,
117 #[serde(skip_serializing_if = "Vec::is_empty")]
118 pub alternative_names: Vec<String>,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct DnsInfo {
123 pub nameservers: Vec<String>,
124 pub mx_records: Vec<String>,
125 pub txt_records: Vec<String>,
126 #[serde(skip_serializing_if = "Option::is_none")]
127 pub spf: Option<String>,
128 #[serde(skip_serializing_if = "Option::is_none")]
129 pub dmarc: Option<String>,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct SecurityInfo {
134 pub https_available: bool,
135 pub https_redirect: bool,
136 pub security_headers: HashMap<String, String>,
137 pub headers_count: usize,
138}
139
140pub async fn get_domain_info(
143 domain: &str,
144 progress_tx: Option<tokio::sync::mpsc::Sender<crate::ScanProgress>>,
145) -> Result<DomainInfoResult, Box<dyn std::error::Error + Send + Sync>> {
146 let clean = clean_domain(domain);
147
148 let client = Client::builder()
149 .timeout(Duration::from_secs(5))
150 .danger_accept_invalid_certs(true)
151 .redirect(reqwest::redirect::Policy::limited(3))
152 .user_agent("Mozilla/5.0")
153 .build()?;
154
155 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; }
156
157 let (mut ipv4, mut all_ipv4, mut ipv6) = (None, vec![], vec![]);
159 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; }
160
161 if let Ok(addrs) = tokio::net::lookup_host(format!("{}:80", clean)).await {
162 for addr in addrs {
163 match addr.ip() {
164 std::net::IpAddr::V4(ip) => {
165 all_ipv4.push(ip.to_string());
166 }
167 std::net::IpAddr::V6(ip) => {
168 ipv6.push(ip.to_string());
169 }
170 }
171 }
172 }
173 if !all_ipv4.is_empty() {
174 ipv4 = Some(all_ipv4[0].clone());
175 }
176 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; }
177
178 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; }
180 let reverse_dns = if let Some(ref ip) = ipv4 {
181 reverse_dns_lookup(ip).await
182 } else {
183 None
184 };
185 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; }
186
187 let whois_fut = async {
189 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; }
190 let res = query_whois(&clean).await;
191 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; }
192 res
193 };
194 let ssl_fut = async {
195 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; }
196 let res = check_ssl(&clean).await;
197 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; }
198 res
199 };
200 let dns_fut = async {
201 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; }
202 let res = get_dns_records(&clean).await;
203 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; }
204 res
205 };
206 let ports_fut = async {
207 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; }
208 let res = scan_ports(ipv4.as_deref()).await;
209 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; }
210 res
211 };
212 let http_fut = async {
213 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; }
214 let res = check_http_status(&client, &clean).await;
215 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; }
216 res
217 };
218 let security_fut = async {
219 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; }
220 let res = check_security(&client, &clean).await;
221 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; }
222 res
223 };
224
225 let (whois, ssl, dns, open_ports, http_info, security) = tokio::join!(
226 whois_fut,
227 ssl_fut,
228 dns_fut,
229 ports_fut,
230 http_fut,
231 security_fut
232 );
233
234 let score = calculate_security_score(&ssl, &dns, &security);
236
237 Ok(DomainInfoResult {
238 domain: clean,
239 ipv4,
240 ipv6,
241 all_ipv4,
242 reverse_dns,
243 whois,
244 ssl,
245 dns,
246 open_ports,
247 http_status: http_info.0,
248 web_server: http_info.1,
249 response_time_ms: http_info.2,
250 security,
251 security_score: score,
252 })
253}
254
255fn clean_domain(domain: &str) -> String {
258 let d = domain
259 .trim_start_matches("https://")
260 .trim_start_matches("http://")
261 .replace("www.", "");
262 d.split('/')
263 .next()
264 .unwrap_or(&d)
265 .split(':')
266 .next()
267 .unwrap_or(&d)
268 .to_string()
269}
270
271async fn reverse_dns_lookup(ip: &str) -> Option<String> {
274 let output = tokio::process::Command::new("dig")
275 .args(["+short", "-x", ip])
276 .output()
277 .await
278 .ok()?;
279 let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
280 if text.is_empty() {
281 None
282 } else {
283 Some(text.trim_end_matches('.').to_string())
284 }
285}
286
287fn get_whois_server(domain: &str) -> &'static str {
290 let tld = domain.split('.').next_back().unwrap_or("");
291 WHOIS_SERVERS
292 .iter()
293 .find(|(t, _)| *t == tld)
294 .map(|(_, s)| *s)
295 .unwrap_or("whois.iana.org")
296}
297
298async fn query_whois_tcp(domain: &str, server: &str) -> Option<String> {
299 let addr = format!("{}:43", server);
300 let mut stream = tokio::time::timeout(Duration::from_secs(10), TcpStream::connect(&addr))
301 .await
302 .ok()?
303 .ok()?;
304
305 stream
306 .write_all(format!("{}\r\n", domain).as_bytes())
307 .await
308 .ok()?;
309
310 let mut buf = Vec::new();
311 let _ = tokio::time::timeout(Duration::from_secs(10), stream.read_to_end(&mut buf)).await;
312
313 Some(String::from_utf8_lossy(&buf).to_string())
314}
315
316async fn query_whois(domain: &str) -> WhoisInfo {
317 let mut info = WhoisInfo {
318 registrar: "Unknown".into(),
319 creation_date: "Unknown".into(),
320 expiry_date: "Unknown".into(),
321 last_updated: "Unknown".into(),
322 domain_status: vec![],
323 registrant: "Unknown".into(),
324 privacy_protection: "Unknown".into(),
325 name_servers: vec![],
326 };
327
328 let server = get_whois_server(domain);
329 let output = match query_whois_tcp(domain, server).await {
330 Some(o) if !o.is_empty() => o,
331 _ => return info,
332 };
333
334 let final_output = if let Some(caps) = Regex::new(r"(?i)Registrar WHOIS Server:\s*(.+)")
336 .ok()
337 .and_then(|r| r.captures(&output))
338 {
339 let referral = caps
340 .get(1)
341 .unwrap()
342 .as_str()
343 .trim()
344 .replace("whois://", "")
345 .replace("http://", "")
346 .replace("https://", "");
347 query_whois_tcp(domain, &referral).await.unwrap_or(output)
348 } else {
349 output
350 };
351
352 for pat in &[
354 r"(?i)Registrar:\s*(.+)",
355 r"(?i)Registrar Name:\s*(.+)",
356 r"(?i)Registrar Organization:\s*(.+)",
357 ] {
358 if let Some(m) = Regex::new(pat).ok().and_then(|r| r.captures(&final_output)) {
359 info.registrar = m.get(1).unwrap().as_str().trim().to_string();
360 break;
361 }
362 }
363
364 for pat in &[
366 r"(?i)Creation Date:\s*(.+)",
367 r"(?i)Created Date:\s*(.+)",
368 r"(?i)Created:\s*(.+)",
369 r"(?i)Registration Time:\s*(.+)",
370 ] {
371 if let Some(m) = Regex::new(pat).ok().and_then(|r| r.captures(&final_output)) {
372 info.creation_date = m
373 .get(1)
374 .unwrap()
375 .as_str()
376 .trim()
377 .split('\n')
378 .next()
379 .unwrap_or("")
380 .to_string();
381 break;
382 }
383 }
384
385 for pat in &[
387 r"(?i)Registry Expiry Date:\s*(.+)",
388 r"(?i)Registrar Registration Expiration Date:\s*(.+)",
389 r"(?i)Expir(?:y|ation) Date:\s*(.+)",
390 r"(?i)expires:\s*(.+)",
391 r"(?i)Expiration Time:\s*(.+)",
392 ] {
393 if let Some(m) = Regex::new(pat).ok().and_then(|r| r.captures(&final_output)) {
394 info.expiry_date = m
395 .get(1)
396 .unwrap()
397 .as_str()
398 .trim()
399 .split('\n')
400 .next()
401 .unwrap_or("")
402 .to_string();
403 break;
404 }
405 }
406
407 for pat in &[
409 r"(?i)Updated Date:\s*(.+)",
410 r"(?i)Last Updated:\s*(.+)",
411 r"(?i)last-update:\s*(.+)",
412 r"(?i)Modified Date:\s*(.+)",
413 ] {
414 if let Some(m) = Regex::new(pat).ok().and_then(|r| r.captures(&final_output)) {
415 info.last_updated = m
416 .get(1)
417 .unwrap()
418 .as_str()
419 .trim()
420 .split('\n')
421 .next()
422 .unwrap_or("")
423 .to_string();
424 break;
425 }
426 }
427
428 if let Ok(rx) = Regex::new(r"(?i)(?:Domain )?Status:\s*(.+)") {
430 info.domain_status = rx
431 .captures_iter(&final_output)
432 .filter_map(|c| {
433 c.get(1).map(|m| {
434 m.as_str()
435 .split_whitespace()
436 .next()
437 .unwrap_or("")
438 .to_string()
439 })
440 })
441 .filter(|s| !s.is_empty())
442 .take(3)
443 .collect();
444 }
445 if info.domain_status.is_empty() {
446 info.domain_status.push("Unknown".into());
447 }
448
449 for pat in &[
451 r"(?i)Registrant Name:\s*(.+)",
452 r"(?i)Registrant:\s*(.+)",
453 r"(?i)Registrant Organization:\s*(.+)",
454 ] {
455 if let Some(m) = Regex::new(pat).ok().and_then(|r| r.captures(&final_output)) {
456 let val = m
457 .get(1)
458 .unwrap()
459 .as_str()
460 .trim()
461 .split('\n')
462 .next()
463 .unwrap_or("")
464 .to_string();
465 if !val.is_empty() {
466 info.registrant = val;
467 break;
468 }
469 }
470 }
471
472 let lower = final_output.to_lowercase();
474 info.privacy_protection = if PRIVACY_KEYWORDS.iter().any(|k| lower.contains(k)) {
475 "Active".into()
476 } else {
477 "Inactive".into()
478 };
479
480 if let Ok(rx) = Regex::new(r"(?i)Name Server:\s*(.+)") {
482 info.name_servers = rx
483 .captures_iter(&final_output)
484 .filter_map(|c| c.get(1).map(|m| m.as_str().trim().to_lowercase()))
485 .take(4)
486 .collect();
487 }
488
489 info
490}
491
492async fn check_ssl(domain: &str) -> SslInfo {
495 let output = match tokio::process::Command::new("openssl")
497 .args([
498 "s_client",
499 "-connect",
500 &format!("{}:443", domain),
501 "-servername",
502 domain,
503 ])
504 .stdin(std::process::Stdio::null())
505 .stdout(std::process::Stdio::piped())
506 .stderr(std::process::Stdio::piped())
507 .output()
508 .await
509 {
510 Ok(o) => String::from_utf8_lossy(&o.stdout).to_string(),
511 Err(_) => {
512 return SslInfo {
513 status: "Error".into(),
514 issued_to: None,
515 issuer: None,
516 protocol_version: None,
517 expiry_date: None,
518 days_until_expiry: None,
519 alternative_names: vec![],
520 }
521 }
522 };
523
524 if output.contains("CONNECTED") {
525 let mut ssl = SslInfo {
526 status: "Valid".into(),
527 issued_to: None,
528 issuer: None,
529 protocol_version: None,
530 expiry_date: None,
531 days_until_expiry: None,
532 alternative_names: vec![],
533 };
534
535 if let Some(m) = Regex::new(r"subject=.*?CN\s*=\s*([^\n/,]+)")
537 .ok()
538 .and_then(|r| r.captures(&output))
539 {
540 ssl.issued_to = Some(m.get(1).unwrap().as_str().trim().to_string());
541 }
542
543 if let Some(m) = Regex::new(r"issuer=.*?CN\s*=\s*([^\n/,]+)")
545 .ok()
546 .and_then(|r| r.captures(&output))
547 {
548 ssl.issuer = Some(m.get(1).unwrap().as_str().trim().to_string());
549 }
550
551 if let Some(m) = Regex::new(r"Protocol\s*:\s*(.+)")
553 .ok()
554 .and_then(|r| r.captures(&output))
555 {
556 ssl.protocol_version = Some(m.get(1).unwrap().as_str().trim().to_string());
557 }
558
559 if let Ok(cert_output) = tokio::process::Command::new("sh")
561 .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)])
562 .output()
563 .await
564 {
565 let cert_text = String::from_utf8_lossy(&cert_output.stdout);
566
567 if let Some(m) = Regex::new(r"notAfter=(.+)").ok().and_then(|r| r.captures(&cert_text)) {
568 let expiry_str = m.get(1).unwrap().as_str().trim().to_string();
569 ssl.expiry_date = Some(expiry_str.clone());
570
571 let clean_expiry = expiry_str.trim_end_matches(" GMT").trim_end_matches(" UTC");
574
575 let parsed_date = chrono::NaiveDateTime::parse_from_str(clean_expiry, "%b %e %H:%M:%S %Y")
577 .or_else(|_| chrono::NaiveDateTime::parse_from_str(clean_expiry, "%b %d %H:%M:%S %Y"));
578
579 if let Ok(expiry) = parsed_date {
580 let now = chrono::Utc::now().naive_utc();
581 ssl.days_until_expiry = Some((expiry - now).num_days());
582 }
583 }
584
585 if let Some(san_section) = cert_text.split("X509v3 Subject Alternative Name:").nth(1) {
587 let names: Vec<String> = Regex::new(r"DNS:([^,\s]+)")
588 .ok()
589 .map(|r| r.captures_iter(san_section).filter_map(|c| c.get(1).map(|m| m.as_str().to_string())).take(5).collect())
590 .unwrap_or_default();
591 ssl.alternative_names = names;
592 }
593 }
594
595 ssl
596 } else {
597 SslInfo {
598 status: "HTTPS not available".into(),
599 issued_to: None,
600 issuer: None,
601 protocol_version: None,
602 expiry_date: None,
603 days_until_expiry: None,
604 alternative_names: vec![],
605 }
606 }
607}
608
609async fn dig_query(domain: &str, rtype: &str) -> Vec<String> {
612 tokio::process::Command::new("dig")
613 .args(["+short", rtype, domain])
614 .output()
615 .await
616 .ok()
617 .and_then(|o| String::from_utf8(o.stdout).ok())
618 .map(|t| {
619 t.lines()
620 .filter(|l| !l.trim().is_empty() && !l.starts_with(';'))
621 .map(|l| l.trim().to_string())
622 .collect()
623 })
624 .unwrap_or_default()
625}
626
627async fn get_dns_records(domain: &str) -> DnsInfo {
628 let (ns, mx, txt) = tokio::join!(
629 dig_query(domain, "NS"),
630 dig_query(domain, "MX"),
631 dig_query(domain, "TXT"),
632 );
633
634 let spf = txt.iter().find(|t| t.contains("v=spf1")).cloned();
635 let dmarc_records = dig_query(&format!("_dmarc.{}", domain), "TXT").await;
636 let dmarc = dmarc_records.into_iter().find(|t| t.contains("v=DMARC1"));
637
638 DnsInfo {
639 nameservers: ns,
640 mx_records: mx,
641 txt_records: txt,
642 spf,
643 dmarc,
644 }
645}
646
647async fn scan_ports(ip: Option<&str>) -> Vec<String> {
650 let ip = match ip {
651 Some(ip) => ip,
652 None => return vec![],
653 };
654
655 let mut results = Vec::new();
656 let mut handles = Vec::new();
657
658 for &(port, service) in COMMON_PORTS {
659 let addr = format!("{}:{}", ip, port);
660 handles.push(tokio::spawn(async move {
661 match tokio::time::timeout(Duration::from_secs(1), TcpStream::connect(&addr)).await {
662 Ok(Ok(_)) => Some(format!("{}/{}", port, service)),
663 _ => None,
664 }
665 }));
666 }
667
668 for handle in handles {
669 if let Ok(Some(port_str)) = handle.await {
670 results.push(port_str);
671 }
672 }
673
674 results.sort();
675 results
676}
677
678async fn check_http_status(
681 client: &Client,
682 domain: &str,
683) -> (Option<String>, Option<String>, Option<f64>) {
684 for proto in &["https", "http"] {
685 let url = format!("{}://{}", proto, domain);
686 let start = Instant::now();
687 match client.get(&url).send().await {
688 Ok(resp) => {
689 let elapsed = start.elapsed().as_secs_f64() * 1000.0;
690 let status_str = format!("{} - {}", resp.status().as_u16(), proto.to_uppercase());
691 let server = resp
692 .headers()
693 .get("server")
694 .and_then(|v| v.to_str().ok())
695 .map(|s| s.to_string());
696 return (
697 Some(status_str),
698 server,
699 Some((elapsed * 100.0).round() / 100.0),
700 );
701 }
702 Err(_) => continue,
703 }
704 }
705 (None, None, None)
706}
707
708async fn check_security(client: &Client, domain: &str) -> SecurityInfo {
711 let mut sec = SecurityInfo {
712 https_available: false,
713 https_redirect: false,
714 security_headers: HashMap::new(),
715 headers_count: 0,
716 };
717
718 if let Ok(resp) = client.get(format!("https://{}", domain)).send().await {
720 sec.https_available = true;
721 for header in SECURITY_HEADERS {
722 if let Some(val) = resp.headers().get(*header) {
723 if let Ok(v) = val.to_str() {
724 sec.security_headers
725 .insert(header.to_string(), v.to_string());
726 sec.headers_count += 1;
727 }
728 }
729 }
730 }
731
732 if let Ok(resp) = client.get(format!("http://{}", domain)).send().await {
734 let final_url = resp.url().to_string();
735 if final_url.starts_with("https://") {
736 sec.https_redirect = true;
737 }
738 }
739
740 sec
741}
742
743fn calculate_security_score(ssl: &SslInfo, dns: &DnsInfo, security: &SecurityInfo) -> u32 {
746 let mut score: u32 = 0;
747
748 if security.https_available {
750 score += 30;
751 }
752
753 if security.https_redirect {
755 score += 10;
756 }
757
758 if ssl.status == "Valid" {
760 score += 20;
761 }
762
763 score += (security.headers_count as u32 * 4).min(20);
765
766 if dns.spf.is_some() {
768 score += 10;
769 }
770
771 if dns.dmarc.is_some() {
773 score += 10;
774 }
775
776 score
777}