1use regex::Regex;
2use reqwest::{Client, Method};
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::time::Duration;
6
7struct WafSignature {
10 name: &'static str,
11 headers: &'static [&'static str],
12 server: &'static [&'static str],
13}
14
15const WAF_SIGNATURES: &[WafSignature] = &[
16 WafSignature {
17 name: "Cloudflare",
18 headers: &["cf-ray", "cf-cache-status", "__cfduid"],
19 server: &["cloudflare"],
20 },
21 WafSignature {
22 name: "Akamai",
23 headers: &["akamai-transformed", "akamai-cache-status"],
24 server: &["akamaighost"],
25 },
26 WafSignature {
27 name: "Imperva Incapsula",
28 headers: &["x-iinfo", "incap_ses"],
29 server: &["imperva"],
30 },
31 WafSignature {
32 name: "Sucuri",
33 headers: &["x-sucuri-id", "x-sucuri-cache"],
34 server: &["sucuri"],
35 },
36 WafSignature {
37 name: "Barracuda",
38 headers: &["barra"],
39 server: &["barracuda"],
40 },
41 WafSignature {
42 name: "F5 BIG-IP",
43 headers: &["f5-http-lb", "bigip"],
44 server: &["bigip", "f5"],
45 },
46 WafSignature {
47 name: "AWS WAF",
48 headers: &["x-amz-cf-id", "x-amzn-requestid"],
49 server: &["awselb"],
50 },
51];
52
53const SECURITY_HEADERS: &[(&str, &str)] = &[
55 ("strict-transport-security", "Critical"),
56 ("content-security-policy", "Critical"),
57 ("x-frame-options", "High"),
58 ("x-content-type-options", "Medium"),
59 ("x-xss-protection", "Medium"),
60 ("referrer-policy", "Medium"),
61 ("permissions-policy", "Medium"),
62];
63
64const ERROR_PATTERNS: &[(&str, &str)] = &[
66 ("fatal error", "PHP Fatal Error"),
67 ("warning.*mysql", "MySQL Warning"),
68 ("error.*sql", "SQL Error"),
69];
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct SecurityAnalysisResult {
75 pub domain: String,
76 pub https_available: bool,
77 pub https_redirect: bool,
78 pub waf_detection: WafDetectionResult,
79 pub security_headers: SecurityHeadersResult,
80 pub ssl_analysis: SslAnalysisResult,
81 pub cors_policy: CorsPolicyResult,
82 pub cookie_security: CookieSecurityResult,
83 pub http_methods: HttpMethodsResult,
84 pub server_information: ServerInfoResult,
85 pub vulnerability_scan: VulnScanResult,
86 pub security_score: SecurityScoreResult,
87 pub recommendations: Vec<String>,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct WafMatch {
92 pub provider: String,
93 pub confidence: String,
94 pub detection_methods: Vec<String>,
95 pub score: u32,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct WafDetectionResult {
100 pub detected: bool,
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub primary_waf: Option<WafMatch>,
103 pub all_detected: Vec<WafMatch>,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct HeaderAnalysis {
108 pub present: bool,
109 pub value: String,
110 pub importance: String,
111 pub security_level: String,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct SecurityHeadersResult {
116 pub headers: HashMap<String, HeaderAnalysis>,
117 pub score: u32,
118 pub missing_critical: Vec<String>,
119 pub missing_high: Vec<String>,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct SslAnalysisResult {
124 pub ssl_available: bool,
125 #[serde(skip_serializing_if = "Option::is_none")]
126 pub protocol_version: Option<String>,
127 #[serde(skip_serializing_if = "Option::is_none")]
128 pub cipher_suite: Option<String>,
129 pub cipher_strength: String,
130 pub overall_grade: String,
131 #[serde(skip_serializing_if = "Option::is_none")]
132 pub subject: Option<String>,
133 #[serde(skip_serializing_if = "Option::is_none")]
134 pub issuer: Option<String>,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct CorsPolicyResult {
139 pub configured: bool,
140 pub headers: HashMap<String, String>,
141 pub issues: Vec<String>,
142 pub security_level: String,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct CookieSecurityResult {
147 pub cookies_present: bool,
148 pub security_issues: Vec<String>,
149 pub security_score: u32,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct HttpMethodsResult {
154 pub methods_detected: bool,
155 pub allowed_methods: Vec<String>,
156 pub dangerous_methods: Vec<String>,
157 pub security_risk: String,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct ServerInfoResult {
162 pub server_headers: HashMap<String, String>,
163 pub information_disclosure: Vec<String>,
164 pub disclosure_count: usize,
165 pub security_level: String,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct VulnerabilityFound {
170 pub vuln_type: String,
171 pub severity: String,
172 pub description: String,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct VulnScanResult {
177 pub vulnerabilities_found: usize,
178 pub vulnerabilities: Vec<VulnerabilityFound>,
179 pub risk_level: String,
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct SecurityScoreResult {
184 pub overall_score: u32,
185 pub grade: String,
186 pub risk_level: String,
187 pub score_breakdown: HashMap<String, u32>,
188}
189
190pub async fn analyze_security(
193 domain: &str,
194 progress_tx: Option<tokio::sync::mpsc::Sender<crate::ScanProgress>>,
195) -> Result<SecurityAnalysisResult, Box<dyn std::error::Error + Send + Sync>> {
196 let clean = if domain.starts_with("http://") || domain.starts_with("https://") {
197 domain
198 .split("//")
199 .nth(1)
200 .unwrap_or(domain)
201 .split('/')
202 .next()
203 .unwrap_or(domain)
204 .to_string()
205 } else {
206 domain.to_string()
207 };
208
209 let client = crate::http_client_builder()
210 .timeout(Duration::from_secs(30))
211 .danger_accept_invalid_certs(true)
212 .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
213 .build()?;
214
215 let http_url = format!("http://{}", clean);
217 let https_url = format!("https://{}", clean);
218
219 let redir_client = crate::http_client_builder()
221 .timeout(Duration::from_secs(15))
222 .danger_accept_invalid_certs(true)
223 .redirect(reqwest::redirect::Policy::none())
224 .user_agent("Mozilla/5.0")
225 .build()?;
226
227 let mut https_redirect = false;
228 if let Ok(resp) = redir_client.get(&http_url).send().await {
229 let status = resp.status().as_u16();
230 if [301, 302, 307, 308].contains(&status) {
231 if let Some(loc) = resp.headers().get("location") {
232 if let Ok(l) = loc.to_str() {
233 if l.starts_with("https://") {
234 https_redirect = true;
235 }
236 }
237 }
238 }
239 }
240
241 let https_resp = client.get(&https_url).send().await;
243 let https_available = https_resp.is_ok();
244
245 let primary = if let Ok(r) = https_resp {
246 r
247 } else {
248 client.get(&http_url).send().await?
249 };
250
251 let resp_url = primary.url().to_string();
252 let headers = primary.headers().clone();
253 let body_text = primary.text().await.unwrap_or_default();
254
255 if let Some(t) = &progress_tx {
257 let _ = t
258 .send(crate::ScanProgress {
259 module: "Security".into(),
260 percentage: 10.0,
261 message: "Detecting Web Application Firewalls (WAF)...".into(),
262 status: "Info".into(),
263 })
264 .await;
265 }
266 let waf_detection = detect_waf(&headers);
267
268 if let Some(t) = &progress_tx {
270 let _ = t
271 .send(crate::ScanProgress {
272 module: "Security".into(),
273 percentage: 20.0,
274 message: "Analyzing HTTP security headers...".into(),
275 status: "Info".into(),
276 })
277 .await;
278 }
279 let security_headers = analyze_security_headers(&headers);
280
281 if let Some(t) = &progress_tx {
283 let _ = t
284 .send(crate::ScanProgress {
285 module: "Security".into(),
286 percentage: 30.0,
287 message: "Evaluating SSL/TLS handshake and ciphers...".into(),
288 status: "Info".into(),
289 })
290 .await;
291 }
292 let ssl_analysis = analyze_ssl(&clean).await;
293
294 if let Some(t) = &progress_tx {
296 let _ = t
297 .send(crate::ScanProgress {
298 module: "Security".into(),
299 percentage: 60.0,
300 message: "Inspecting CORS policy configuration...".into(),
301 status: "Info".into(),
302 })
303 .await;
304 }
305 let cors_policy = analyze_cors(&headers);
306
307 if let Some(t) = &progress_tx {
309 let _ = t
310 .send(crate::ScanProgress {
311 module: "Security".into(),
312 percentage: 70.0,
313 message: "Checking cookie security flags...".into(),
314 status: "Info".into(),
315 })
316 .await;
317 }
318 let cookie_security = analyze_cookies(&headers);
319
320 if let Some(t) = &progress_tx {
322 let _ = t
323 .send(crate::ScanProgress {
324 module: "Security".into(),
325 percentage: 75.0,
326 message: "Discovering allowed HTTP methods...".into(),
327 status: "Info".into(),
328 })
329 .await;
330 }
331 let http_methods = detect_methods(&client, &https_url).await;
332
333 if let Some(t) = &progress_tx {
335 let _ = t
336 .send(crate::ScanProgress {
337 module: "Security".into(),
338 percentage: 85.0,
339 message: "Looking for server information disclosure...".into(),
340 status: "Info".into(),
341 })
342 .await;
343 }
344 let server_information = analyze_server_info(&headers);
345
346 if let Some(t) = &progress_tx {
348 let _ = t
349 .send(crate::ScanProgress {
350 module: "Security".into(),
351 percentage: 90.0,
352 message: "Performing basic vulnerability pattern matching...".into(),
353 status: "Info".into(),
354 })
355 .await;
356 }
357 let vulnerability_scan = perform_vuln_scan(&resp_url, &body_text);
358
359 if let Some(t) = &progress_tx {
361 let _ = t
362 .send(crate::ScanProgress {
363 module: "Security".into(),
364 percentage: 95.0,
365 message: "Calculating final security score...".into(),
366 status: "Info".into(),
367 })
368 .await;
369 }
370 let security_score = calculate_score(
371 &security_headers,
372 &ssl_analysis,
373 &waf_detection,
374 &vulnerability_scan,
375 );
376 let recommendations = generate_recommendations(
377 &security_headers,
378 &ssl_analysis,
379 &waf_detection,
380 https_available,
381 https_redirect,
382 );
383
384 Ok(SecurityAnalysisResult {
385 domain: clean,
386 https_available,
387 https_redirect,
388 waf_detection,
389 security_headers,
390 ssl_analysis,
391 cors_policy,
392 cookie_security,
393 http_methods,
394 server_information,
395 vulnerability_scan,
396 security_score,
397 recommendations,
398 })
399}
400
401fn detect_waf(headers: &reqwest::header::HeaderMap) -> WafDetectionResult {
404 let headers_str = format!("{:?}", headers).to_lowercase();
405 let server_header = headers
406 .get("server")
407 .and_then(|v| v.to_str().ok())
408 .unwrap_or("")
409 .to_lowercase();
410
411 let mut detected = Vec::new();
412
413 for sig in WAF_SIGNATURES {
414 let mut confidence: u32 = 0;
415 let mut methods = Vec::new();
416
417 for h in sig.headers {
418 if headers_str.contains(h) {
419 confidence += 40;
420 methods.push(format!("Header: {}", h));
421 }
422 }
423 for s in sig.server {
424 if server_header.contains(s) {
425 confidence += 30;
426 methods.push(format!("Server: {}", s));
427 }
428 }
429
430 if confidence > 0 {
431 let conf_str = if confidence >= 50 {
432 "High"
433 } else if confidence >= 30 {
434 "Medium"
435 } else {
436 "Low"
437 };
438 detected.push(WafMatch {
439 provider: sig.name.to_string(),
440 confidence: conf_str.into(),
441 detection_methods: methods,
442 score: confidence,
443 });
444 }
445 }
446
447 detected.sort_by_key(|waf_match| std::cmp::Reverse(waf_match.score));
448
449 WafDetectionResult {
450 detected: !detected.is_empty(),
451 primary_waf: detected.first().cloned(),
452 all_detected: detected,
453 }
454}
455
456fn analyze_security_headers(headers: &reqwest::header::HeaderMap) -> SecurityHeadersResult {
459 let mut analysis = HashMap::new();
460 let mut total_score: u32 = 0;
461 let mut max_score: u32 = 0;
462 let mut missing_critical = Vec::new();
463 let mut missing_high = Vec::new();
464
465 for &(name, importance) in SECURITY_HEADERS {
466 let present = headers.get(name).is_some();
467 let value = headers
468 .get(name)
469 .and_then(|v| v.to_str().ok())
470 .unwrap_or("Not Set")
471 .to_string();
472
473 let security_level = if present {
474 "Good".into()
475 } else if importance == "Critical" {
476 "Critical".into()
477 } else {
478 "Medium".into()
479 };
480
481 let weight = match importance {
482 "Critical" => 30,
483 "High" => 20,
484 _ => 10,
485 };
486 max_score += weight;
487 if present {
488 total_score += weight;
489 } else if importance == "Critical" {
490 missing_critical.push(name.to_string());
491 } else if importance == "High" {
492 missing_high.push(name.to_string());
493 }
494
495 analysis.insert(
496 name.to_string(),
497 HeaderAnalysis {
498 present,
499 value,
500 importance: importance.into(),
501 security_level,
502 },
503 );
504 }
505
506 let score = total_score
507 .saturating_mul(100)
508 .checked_div(max_score)
509 .unwrap_or(0);
510
511 SecurityHeadersResult {
512 headers: analysis,
513 score,
514 missing_critical,
515 missing_high,
516 }
517}
518
519async fn analyze_ssl(domain: &str) -> SslAnalysisResult {
522 let output = match tokio::process::Command::new("openssl")
523 .args([
524 "s_client",
525 "-connect",
526 &format!("{}:443", domain),
527 "-servername",
528 domain,
529 ])
530 .stdin(std::process::Stdio::null())
531 .stdout(std::process::Stdio::piped())
532 .stderr(std::process::Stdio::piped())
533 .output()
534 .await
535 {
536 Ok(o) => String::from_utf8_lossy(&o.stdout).to_string(),
537 Err(_) => {
538 return SslAnalysisResult {
539 ssl_available: false,
540 protocol_version: None,
541 cipher_suite: None,
542 cipher_strength: "Unknown".into(),
543 overall_grade: "F".into(),
544 subject: None,
545 issuer: None,
546 }
547 }
548 };
549
550 if !output.contains("CONNECTED") {
551 return SslAnalysisResult {
552 ssl_available: false,
553 protocol_version: None,
554 cipher_suite: None,
555 cipher_strength: "Unknown".into(),
556 overall_grade: "F".into(),
557 subject: None,
558 issuer: None,
559 };
560 }
561
562 let protocol = Regex::new(r"Protocol\s*:\s*(.+)")
563 .ok()
564 .and_then(|r| r.captures(&output))
565 .and_then(|c| c.get(1).map(|m| m.as_str().trim().to_string()));
566
567 let cipher_suite = Regex::new(r"Cipher\s*:\s*(.+)")
568 .ok()
569 .and_then(|r| r.captures(&output))
570 .and_then(|c| c.get(1).map(|m| m.as_str().trim().to_string()));
571
572 let subject = Regex::new(r"subject=.*?CN\s*=\s*([^\n/,]+)")
573 .ok()
574 .and_then(|r| r.captures(&output))
575 .and_then(|c| c.get(1).map(|m| m.as_str().trim().to_string()));
576
577 let issuer = Regex::new(r"issuer=.*?CN\s*=\s*([^\n/,]+)")
578 .ok()
579 .and_then(|r| r.captures(&output))
580 .and_then(|c| c.get(1).map(|m| m.as_str().trim().to_string()));
581
582 let cipher_strength = match &cipher_suite {
584 Some(c) if c.contains("AES256") || c.contains("CHACHA20") || c.contains("TLS_AES_256") => {
585 "Strong"
586 }
587 Some(c) if c.contains("AES128") => "Medium",
588 Some(c) if c.contains("DES") || c.contains("RC4") || c.contains("NULL") => "Weak",
589 _ => "Unknown",
590 };
591
592 let proto_str = protocol.as_deref().unwrap_or("");
594 let grade = if proto_str.contains("TLSv1.3") {
595 "A+"
596 } else if proto_str.contains("TLSv1.2") && cipher_strength == "Strong" {
597 "A"
598 } else if proto_str.contains("TLSv1.2") {
599 "B"
600 } else if proto_str.contains("TLSv1.1") || proto_str.contains("TLSv1") {
601 "C"
602 } else {
603 "F"
604 };
605
606 SslAnalysisResult {
607 ssl_available: true,
608 protocol_version: protocol,
609 cipher_suite,
610 cipher_strength: cipher_strength.into(),
611 overall_grade: grade.into(),
612 subject,
613 issuer,
614 }
615}
616
617fn analyze_cors(headers: &reqwest::header::HeaderMap) -> CorsPolicyResult {
620 let cors_keys = [
621 "access-control-allow-origin",
622 "access-control-allow-methods",
623 "access-control-allow-headers",
624 "access-control-allow-credentials",
625 ];
626
627 let mut cors_headers = HashMap::new();
628 let mut configured = false;
629 let mut issues = Vec::new();
630
631 for &key in &cors_keys {
632 let val = headers
633 .get(key)
634 .and_then(|v| v.to_str().ok())
635 .unwrap_or("Not Set")
636 .to_string();
637 if val != "Not Set" {
638 configured = true;
639 }
640 cors_headers.insert(key.to_string(), val);
641 }
642
643 let origin = cors_headers
644 .get("access-control-allow-origin")
645 .map(|s| s.as_str())
646 .unwrap_or("Not Set");
647 let creds = cors_headers
648 .get("access-control-allow-credentials")
649 .map(|s| s.as_str())
650 .unwrap_or("Not Set");
651
652 if origin == "*" && creds == "true" {
653 issues.push("Critical: Wildcard origin with credentials allowed".into());
654 } else if origin == "*" {
655 issues.push("Warning: Wildcard origin allows all domains".into());
656 }
657
658 let security_level = if issues.is_empty() {
659 "High"
660 } else if issues.len() <= 1 {
661 "Medium"
662 } else {
663 "Low"
664 };
665
666 CorsPolicyResult {
667 configured,
668 headers: cors_headers,
669 issues,
670 security_level: security_level.into(),
671 }
672}
673
674fn analyze_cookies(headers: &reqwest::header::HeaderMap) -> CookieSecurityResult {
677 let cookie_val = match headers.get("set-cookie").and_then(|v| v.to_str().ok()) {
678 Some(c) => c.to_string(),
679 None => {
680 return CookieSecurityResult {
681 cookies_present: false,
682 security_issues: vec![],
683 security_score: 100,
684 }
685 }
686 };
687
688 let mut issues = Vec::new();
689 if !cookie_val.contains("Secure") {
690 issues.push("Missing Secure flag".into());
691 }
692 if !cookie_val.contains("HttpOnly") {
693 issues.push("Missing HttpOnly flag".into());
694 }
695 if !cookie_val.contains("SameSite") {
696 issues.push("Missing SameSite attribute".into());
697 }
698
699 let score = 100u32.saturating_sub(issues.len() as u32 * 25);
700
701 CookieSecurityResult {
702 cookies_present: true,
703 security_issues: issues,
704 security_score: score,
705 }
706}
707
708async fn detect_methods(client: &Client, url: &str) -> HttpMethodsResult {
711 let dangerous = ["DELETE", "PUT", "PATCH", "TRACE", "CONNECT"];
712
713 match client.request(Method::OPTIONS, url).send().await {
714 Ok(resp) => {
715 let allow = resp
716 .headers()
717 .get("allow")
718 .and_then(|v| v.to_str().ok())
719 .unwrap_or("");
720 let methods: Vec<String> = allow
721 .split(',')
722 .map(|m| m.trim().to_string())
723 .filter(|m| !m.is_empty())
724 .collect();
725
726 let found_dangerous: Vec<String> = methods
727 .iter()
728 .filter(|m| dangerous.contains(&m.to_uppercase().as_str()))
729 .cloned()
730 .collect();
731
732 let risk = if !found_dangerous.is_empty() {
733 "High"
734 } else {
735 "Low"
736 };
737
738 HttpMethodsResult {
739 methods_detected: true,
740 allowed_methods: methods,
741 dangerous_methods: found_dangerous,
742 security_risk: risk.into(),
743 }
744 }
745 Err(_) => HttpMethodsResult {
746 methods_detected: false,
747 allowed_methods: vec![],
748 dangerous_methods: vec![],
749 security_risk: "Unknown".into(),
750 },
751 }
752}
753
754fn analyze_server_info(headers: &reqwest::header::HeaderMap) -> ServerInfoResult {
757 let disclosure_headers = [
758 ("server", "Web server version disclosed"),
759 ("x-powered-by", "Technology stack disclosed"),
760 ];
761
762 let mut server_headers = HashMap::new();
763 let mut issues = Vec::new();
764
765 for &(header, issue) in &disclosure_headers {
766 if let Some(val) = headers.get(header).and_then(|v| v.to_str().ok()) {
767 server_headers.insert(header.to_string(), val.to_string());
768 issues.push(issue.to_string());
769 }
770 }
771
772 let count = issues.len();
773 let level = if count > 2 {
774 "High"
775 } else if count > 0 {
776 "Medium"
777 } else {
778 "Good"
779 };
780
781 ServerInfoResult {
782 server_headers,
783 information_disclosure: issues,
784 disclosure_count: count,
785 security_level: level.into(),
786 }
787}
788
789fn perform_vuln_scan(resp_url: &str, body: &str) -> VulnScanResult {
792 let mut vulns = Vec::new();
793
794 if !resp_url.starts_with("https://") {
796 vulns.push(VulnerabilityFound {
797 vuln_type: "Insecure Transport".into(),
798 severity: "High".into(),
799 description: "Site not enforcing HTTPS".into(),
800 });
801 }
802
803 for &(pattern, desc) in ERROR_PATTERNS {
805 if let Ok(rx) = Regex::new(&format!("(?i){}", pattern)) {
806 if rx.is_match(body) {
807 vulns.push(VulnerabilityFound {
808 vuln_type: "Information Disclosure".into(),
809 severity: "Low".into(),
810 description: format!("{} detected in response", desc),
811 });
812 }
813 }
814 }
815
816 let risk = calculate_risk_level(&vulns);
817
818 VulnScanResult {
819 vulnerabilities_found: vulns.len(),
820 vulnerabilities: vulns,
821 risk_level: risk,
822 }
823}
824
825fn calculate_risk_level(vulns: &[VulnerabilityFound]) -> String {
826 if vulns.is_empty() {
827 return "Low".into();
828 }
829 let total: u32 = vulns
830 .iter()
831 .map(|v| match v.severity.as_str() {
832 "High" => 3,
833 "Medium" => 2,
834 _ => 1,
835 })
836 .sum();
837
838 if total >= 6 {
839 "Critical".into()
840 } else if total >= 4 {
841 "High".into()
842 } else if total >= 2 {
843 "Medium".into()
844 } else {
845 "Low".into()
846 }
847}
848
849fn calculate_score(
852 headers: &SecurityHeadersResult,
853 ssl: &SslAnalysisResult,
854 waf: &WafDetectionResult,
855 vulns: &VulnScanResult,
856) -> SecurityScoreResult {
857 let mut breakdown = HashMap::new();
858 let mut total: f64 = 100.0;
859
860 let h_score = headers.score;
862 breakdown.insert("security_headers".into(), h_score);
863 total -= (100.0 - h_score as f64) * 0.4;
864
865 let ssl_score: u32 = match ssl.overall_grade.as_str() {
867 "A+" => 100,
868 "A" => 90,
869 "B" => 75,
870 "C" => 60,
871 "D" => 40,
872 _ => 0,
873 };
874 breakdown.insert("ssl_tls".into(), ssl_score);
875 total -= (100.0 - ssl_score as f64) * 0.3;
876
877 let waf_score: u32 = if waf.detected { 100 } else { 60 };
879 breakdown.insert("waf_protection".into(), waf_score);
880 total -= (100.0 - waf_score as f64) * 0.15;
881
882 let vuln_score = 100u32.saturating_sub(vulns.vulnerabilities_found as u32 * 20);
884 breakdown.insert("vulnerabilities".into(), vuln_score);
885 total -= (100.0 - vuln_score as f64) * 0.15;
886
887 let final_score = total.clamp(0.0, 100.0) as u32;
888
889 let grade = if final_score >= 95 {
890 "A+"
891 } else if final_score >= 90 {
892 "A"
893 } else if final_score >= 80 {
894 "B"
895 } else if final_score >= 70 {
896 "C"
897 } else if final_score >= 60 {
898 "D"
899 } else {
900 "F"
901 };
902
903 let risk = if final_score >= 85 {
904 "Low Risk"
905 } else if final_score >= 70 {
906 "Medium Risk"
907 } else if final_score >= 50 {
908 "High Risk"
909 } else {
910 "Critical Risk"
911 };
912
913 SecurityScoreResult {
914 overall_score: final_score,
915 grade: grade.into(),
916 risk_level: risk.into(),
917 score_breakdown: breakdown,
918 }
919}
920
921fn generate_recommendations(
924 headers: &SecurityHeadersResult,
925 ssl: &SslAnalysisResult,
926 waf: &WafDetectionResult,
927 https_available: bool,
928 https_redirect: bool,
929) -> Vec<String> {
930 let mut recs = Vec::new();
931
932 if !headers.missing_critical.is_empty() {
933 recs.push(format!(
934 "CRITICAL: Implement missing security headers: {}",
935 headers.missing_critical.join(", ")
936 ));
937 }
938 if !headers.missing_high.is_empty() {
939 recs.push(format!(
940 "HIGH: Add security headers: {}",
941 headers.missing_high.join(", ")
942 ));
943 }
944
945 match ssl.overall_grade.as_str() {
946 "D" | "F" => recs.push("CRITICAL: Upgrade SSL/TLS configuration".into()),
947 "C" => recs.push("MEDIUM: Consider improving SSL/TLS configuration".into()),
948 _ => {}
949 }
950
951 if !waf.detected {
952 recs.push("MEDIUM: Consider implementing a Web Application Firewall (WAF)".into());
953 }
954
955 if !https_available {
956 recs.push("CRITICAL: Enable HTTPS for secure communication".into());
957 } else if !https_redirect {
958 recs.push("MEDIUM: Implement automatic HTTP to HTTPS redirect".into());
959 }
960
961 recs.truncate(10);
962 recs
963}