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 = 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 = 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 { let _ = t.send(crate::ScanProgress { module: "Security".into(), percentage: 10.0, message: "Detecting Web Application Firewalls (WAF)...".into(), status: "Info".into() }).await; }
257 let waf_detection = detect_waf(&headers);
258
259 if let Some(t) = &progress_tx { let _ = t.send(crate::ScanProgress { module: "Security".into(), percentage: 20.0, message: "Analyzing HTTP security headers...".into(), status: "Info".into() }).await; }
261 let security_headers = analyze_security_headers(&headers);
262
263 if let Some(t) = &progress_tx { let _ = t.send(crate::ScanProgress { module: "Security".into(), percentage: 30.0, message: "Evaluating SSL/TLS handshake and ciphers...".into(), status: "Info".into() }).await; }
265 let ssl_analysis = analyze_ssl(&clean).await;
266
267 if let Some(t) = &progress_tx { let _ = t.send(crate::ScanProgress { module: "Security".into(), percentage: 60.0, message: "Inspecting CORS policy configuration...".into(), status: "Info".into() }).await; }
269 let cors_policy = analyze_cors(&headers);
270
271 if let Some(t) = &progress_tx { let _ = t.send(crate::ScanProgress { module: "Security".into(), percentage: 70.0, message: "Checking cookie security flags...".into(), status: "Info".into() }).await; }
273 let cookie_security = analyze_cookies(&headers);
274
275 if let Some(t) = &progress_tx { let _ = t.send(crate::ScanProgress { module: "Security".into(), percentage: 75.0, message: "Discovering allowed HTTP methods...".into(), status: "Info".into() }).await; }
277 let http_methods = detect_methods(&client, &https_url).await;
278
279 if let Some(t) = &progress_tx { let _ = t.send(crate::ScanProgress { module: "Security".into(), percentage: 85.0, message: "Looking for server information disclosure...".into(), status: "Info".into() }).await; }
281 let server_information = analyze_server_info(&headers);
282
283 if let Some(t) = &progress_tx { let _ = t.send(crate::ScanProgress { module: "Security".into(), percentage: 90.0, message: "Performing basic vulnerability pattern matching...".into(), status: "Info".into() }).await; }
285 let vulnerability_scan = perform_vuln_scan(&resp_url, &body_text);
286
287 if let Some(t) = &progress_tx { let _ = t.send(crate::ScanProgress { module: "Security".into(), percentage: 95.0, message: "Calculating final security score...".into(), status: "Info".into() }).await; }
289 let security_score = calculate_score(
290 &security_headers,
291 &ssl_analysis,
292 &waf_detection,
293 &vulnerability_scan,
294 );
295 let recommendations = generate_recommendations(
296 &security_headers,
297 &ssl_analysis,
298 &waf_detection,
299 https_available,
300 https_redirect,
301 );
302
303 Ok(SecurityAnalysisResult {
304 domain: clean,
305 https_available,
306 https_redirect,
307 waf_detection,
308 security_headers,
309 ssl_analysis,
310 cors_policy,
311 cookie_security,
312 http_methods,
313 server_information,
314 vulnerability_scan,
315 security_score,
316 recommendations,
317 })
318}
319
320fn detect_waf(headers: &reqwest::header::HeaderMap) -> WafDetectionResult {
323 let headers_str = format!("{:?}", headers).to_lowercase();
324 let server_header = headers
325 .get("server")
326 .and_then(|v| v.to_str().ok())
327 .unwrap_or("")
328 .to_lowercase();
329
330 let mut detected = Vec::new();
331
332 for sig in WAF_SIGNATURES {
333 let mut confidence: u32 = 0;
334 let mut methods = Vec::new();
335
336 for h in sig.headers {
337 if headers_str.contains(h) {
338 confidence += 40;
339 methods.push(format!("Header: {}", h));
340 }
341 }
342 for s in sig.server {
343 if server_header.contains(s) {
344 confidence += 30;
345 methods.push(format!("Server: {}", s));
346 }
347 }
348
349 if confidence > 0 {
350 let conf_str = if confidence >= 50 {
351 "High"
352 } else if confidence >= 30 {
353 "Medium"
354 } else {
355 "Low"
356 };
357 detected.push(WafMatch {
358 provider: sig.name.to_string(),
359 confidence: conf_str.into(),
360 detection_methods: methods,
361 score: confidence,
362 });
363 }
364 }
365
366 detected.sort_by(|a, b| b.score.cmp(&a.score));
367
368 WafDetectionResult {
369 detected: !detected.is_empty(),
370 primary_waf: detected.first().cloned(),
371 all_detected: detected,
372 }
373}
374
375fn analyze_security_headers(headers: &reqwest::header::HeaderMap) -> SecurityHeadersResult {
378 let mut analysis = HashMap::new();
379 let mut total_score: u32 = 0;
380 let mut max_score: u32 = 0;
381 let mut missing_critical = Vec::new();
382 let mut missing_high = Vec::new();
383
384 for &(name, importance) in SECURITY_HEADERS {
385 let present = headers.get(name).is_some();
386 let value = headers
387 .get(name)
388 .and_then(|v| v.to_str().ok())
389 .unwrap_or("Not Set")
390 .to_string();
391
392 let security_level = if present {
393 "Good".into()
394 } else if importance == "Critical" {
395 "Critical".into()
396 } else {
397 "Medium".into()
398 };
399
400 let weight = match importance {
401 "Critical" => 30,
402 "High" => 20,
403 _ => 10,
404 };
405 max_score += weight;
406 if present {
407 total_score += weight;
408 } else if importance == "Critical" {
409 missing_critical.push(name.to_string());
410 } else if importance == "High" {
411 missing_high.push(name.to_string());
412 }
413
414 analysis.insert(
415 name.to_string(),
416 HeaderAnalysis {
417 present,
418 value,
419 importance: importance.into(),
420 security_level,
421 },
422 );
423 }
424
425 let score = if max_score > 0 {
426 total_score * 100 / max_score
427 } else {
428 0
429 };
430
431 SecurityHeadersResult {
432 headers: analysis,
433 score,
434 missing_critical,
435 missing_high,
436 }
437}
438
439async fn analyze_ssl(domain: &str) -> SslAnalysisResult {
442 let output = match tokio::process::Command::new("openssl")
443 .args([
444 "s_client",
445 "-connect",
446 &format!("{}:443", domain),
447 "-servername",
448 domain,
449 ])
450 .stdin(std::process::Stdio::null())
451 .stdout(std::process::Stdio::piped())
452 .stderr(std::process::Stdio::piped())
453 .output()
454 .await
455 {
456 Ok(o) => String::from_utf8_lossy(&o.stdout).to_string(),
457 Err(_) => {
458 return SslAnalysisResult {
459 ssl_available: false,
460 protocol_version: None,
461 cipher_suite: None,
462 cipher_strength: "Unknown".into(),
463 overall_grade: "F".into(),
464 subject: None,
465 issuer: None,
466 }
467 }
468 };
469
470 if !output.contains("CONNECTED") {
471 return SslAnalysisResult {
472 ssl_available: false,
473 protocol_version: None,
474 cipher_suite: None,
475 cipher_strength: "Unknown".into(),
476 overall_grade: "F".into(),
477 subject: None,
478 issuer: None,
479 };
480 }
481
482 let protocol = Regex::new(r"Protocol\s*:\s*(.+)")
483 .ok()
484 .and_then(|r| r.captures(&output))
485 .and_then(|c| c.get(1).map(|m| m.as_str().trim().to_string()));
486
487 let cipher_suite = Regex::new(r"Cipher\s*:\s*(.+)")
488 .ok()
489 .and_then(|r| r.captures(&output))
490 .and_then(|c| c.get(1).map(|m| m.as_str().trim().to_string()));
491
492 let subject = Regex::new(r"subject=.*?CN\s*=\s*([^\n/,]+)")
493 .ok()
494 .and_then(|r| r.captures(&output))
495 .and_then(|c| c.get(1).map(|m| m.as_str().trim().to_string()));
496
497 let issuer = Regex::new(r"issuer=.*?CN\s*=\s*([^\n/,]+)")
498 .ok()
499 .and_then(|r| r.captures(&output))
500 .and_then(|c| c.get(1).map(|m| m.as_str().trim().to_string()));
501
502 let cipher_strength = match &cipher_suite {
504 Some(c) if c.contains("AES256") || c.contains("CHACHA20") || c.contains("TLS_AES_256") => {
505 "Strong"
506 }
507 Some(c) if c.contains("AES128") => "Medium",
508 Some(c) if c.contains("DES") || c.contains("RC4") || c.contains("NULL") => "Weak",
509 _ => "Unknown",
510 };
511
512 let proto_str = protocol.as_deref().unwrap_or("");
514 let grade = if proto_str.contains("TLSv1.3") {
515 "A+"
516 } else if proto_str.contains("TLSv1.2") && cipher_strength == "Strong" {
517 "A"
518 } else if proto_str.contains("TLSv1.2") {
519 "B"
520 } else if proto_str.contains("TLSv1.1") || proto_str.contains("TLSv1") {
521 "C"
522 } else {
523 "F"
524 };
525
526 SslAnalysisResult {
527 ssl_available: true,
528 protocol_version: protocol,
529 cipher_suite,
530 cipher_strength: cipher_strength.into(),
531 overall_grade: grade.into(),
532 subject,
533 issuer,
534 }
535}
536
537fn analyze_cors(headers: &reqwest::header::HeaderMap) -> CorsPolicyResult {
540 let cors_keys = [
541 "access-control-allow-origin",
542 "access-control-allow-methods",
543 "access-control-allow-headers",
544 "access-control-allow-credentials",
545 ];
546
547 let mut cors_headers = HashMap::new();
548 let mut configured = false;
549 let mut issues = Vec::new();
550
551 for &key in &cors_keys {
552 let val = headers
553 .get(key)
554 .and_then(|v| v.to_str().ok())
555 .unwrap_or("Not Set")
556 .to_string();
557 if val != "Not Set" {
558 configured = true;
559 }
560 cors_headers.insert(key.to_string(), val);
561 }
562
563 let origin = cors_headers
564 .get("access-control-allow-origin")
565 .map(|s| s.as_str())
566 .unwrap_or("Not Set");
567 let creds = cors_headers
568 .get("access-control-allow-credentials")
569 .map(|s| s.as_str())
570 .unwrap_or("Not Set");
571
572 if origin == "*" && creds == "true" {
573 issues.push("Critical: Wildcard origin with credentials allowed".into());
574 } else if origin == "*" {
575 issues.push("Warning: Wildcard origin allows all domains".into());
576 }
577
578 let security_level = if issues.is_empty() {
579 "High"
580 } else if issues.len() <= 1 {
581 "Medium"
582 } else {
583 "Low"
584 };
585
586 CorsPolicyResult {
587 configured,
588 headers: cors_headers,
589 issues,
590 security_level: security_level.into(),
591 }
592}
593
594fn analyze_cookies(headers: &reqwest::header::HeaderMap) -> CookieSecurityResult {
597 let cookie_val = match headers.get("set-cookie").and_then(|v| v.to_str().ok()) {
598 Some(c) => c.to_string(),
599 None => {
600 return CookieSecurityResult {
601 cookies_present: false,
602 security_issues: vec![],
603 security_score: 100,
604 }
605 }
606 };
607
608 let mut issues = Vec::new();
609 if !cookie_val.contains("Secure") {
610 issues.push("Missing Secure flag".into());
611 }
612 if !cookie_val.contains("HttpOnly") {
613 issues.push("Missing HttpOnly flag".into());
614 }
615 if !cookie_val.contains("SameSite") {
616 issues.push("Missing SameSite attribute".into());
617 }
618
619 let score = 100u32.saturating_sub(issues.len() as u32 * 25);
620
621 CookieSecurityResult {
622 cookies_present: true,
623 security_issues: issues,
624 security_score: score,
625 }
626}
627
628async fn detect_methods(client: &Client, url: &str) -> HttpMethodsResult {
631 let dangerous = ["DELETE", "PUT", "PATCH", "TRACE", "CONNECT"];
632
633 match client.request(Method::OPTIONS, url).send().await {
634 Ok(resp) => {
635 let allow = resp
636 .headers()
637 .get("allow")
638 .and_then(|v| v.to_str().ok())
639 .unwrap_or("");
640 let methods: Vec<String> = allow
641 .split(',')
642 .map(|m| m.trim().to_string())
643 .filter(|m| !m.is_empty())
644 .collect();
645
646 let found_dangerous: Vec<String> = methods
647 .iter()
648 .filter(|m| dangerous.contains(&m.to_uppercase().as_str()))
649 .cloned()
650 .collect();
651
652 let risk = if !found_dangerous.is_empty() {
653 "High"
654 } else {
655 "Low"
656 };
657
658 HttpMethodsResult {
659 methods_detected: true,
660 allowed_methods: methods,
661 dangerous_methods: found_dangerous,
662 security_risk: risk.into(),
663 }
664 }
665 Err(_) => HttpMethodsResult {
666 methods_detected: false,
667 allowed_methods: vec![],
668 dangerous_methods: vec![],
669 security_risk: "Unknown".into(),
670 },
671 }
672}
673
674fn analyze_server_info(headers: &reqwest::header::HeaderMap) -> ServerInfoResult {
677 let disclosure_headers = [
678 ("server", "Web server version disclosed"),
679 ("x-powered-by", "Technology stack disclosed"),
680 ];
681
682 let mut server_headers = HashMap::new();
683 let mut issues = Vec::new();
684
685 for &(header, issue) in &disclosure_headers {
686 if let Some(val) = headers.get(header).and_then(|v| v.to_str().ok()) {
687 server_headers.insert(header.to_string(), val.to_string());
688 issues.push(issue.to_string());
689 }
690 }
691
692 let count = issues.len();
693 let level = if count > 2 {
694 "High"
695 } else if count > 0 {
696 "Medium"
697 } else {
698 "Good"
699 };
700
701 ServerInfoResult {
702 server_headers,
703 information_disclosure: issues,
704 disclosure_count: count,
705 security_level: level.into(),
706 }
707}
708
709fn perform_vuln_scan(resp_url: &str, body: &str) -> VulnScanResult {
712 let mut vulns = Vec::new();
713
714 if !resp_url.starts_with("https://") {
716 vulns.push(VulnerabilityFound {
717 vuln_type: "Insecure Transport".into(),
718 severity: "High".into(),
719 description: "Site not enforcing HTTPS".into(),
720 });
721 }
722
723 for &(pattern, desc) in ERROR_PATTERNS {
725 if let Ok(rx) = Regex::new(&format!("(?i){}", pattern)) {
726 if rx.is_match(body) {
727 vulns.push(VulnerabilityFound {
728 vuln_type: "Information Disclosure".into(),
729 severity: "Low".into(),
730 description: format!("{} detected in response", desc),
731 });
732 }
733 }
734 }
735
736 let risk = calculate_risk_level(&vulns);
737
738 VulnScanResult {
739 vulnerabilities_found: vulns.len(),
740 vulnerabilities: vulns,
741 risk_level: risk,
742 }
743}
744
745fn calculate_risk_level(vulns: &[VulnerabilityFound]) -> String {
746 if vulns.is_empty() {
747 return "Low".into();
748 }
749 let total: u32 = vulns
750 .iter()
751 .map(|v| match v.severity.as_str() {
752 "High" => 3,
753 "Medium" => 2,
754 _ => 1,
755 })
756 .sum();
757
758 if total >= 6 {
759 "Critical".into()
760 } else if total >= 4 {
761 "High".into()
762 } else if total >= 2 {
763 "Medium".into()
764 } else {
765 "Low".into()
766 }
767}
768
769fn calculate_score(
772 headers: &SecurityHeadersResult,
773 ssl: &SslAnalysisResult,
774 waf: &WafDetectionResult,
775 vulns: &VulnScanResult,
776) -> SecurityScoreResult {
777 let mut breakdown = HashMap::new();
778 let mut total: f64 = 100.0;
779
780 let h_score = headers.score;
782 breakdown.insert("security_headers".into(), h_score);
783 total -= (100.0 - h_score as f64) * 0.4;
784
785 let ssl_score: u32 = match ssl.overall_grade.as_str() {
787 "A+" => 100,
788 "A" => 90,
789 "B" => 75,
790 "C" => 60,
791 "D" => 40,
792 _ => 0,
793 };
794 breakdown.insert("ssl_tls".into(), ssl_score);
795 total -= (100.0 - ssl_score as f64) * 0.3;
796
797 let waf_score: u32 = if waf.detected { 100 } else { 60 };
799 breakdown.insert("waf_protection".into(), waf_score);
800 total -= (100.0 - waf_score as f64) * 0.15;
801
802 let vuln_score = 100u32.saturating_sub(vulns.vulnerabilities_found as u32 * 20);
804 breakdown.insert("vulnerabilities".into(), vuln_score);
805 total -= (100.0 - vuln_score as f64) * 0.15;
806
807 let final_score = total.clamp(0.0, 100.0) as u32;
808
809 let grade = if final_score >= 95 {
810 "A+"
811 } else if final_score >= 90 {
812 "A"
813 } else if final_score >= 80 {
814 "B"
815 } else if final_score >= 70 {
816 "C"
817 } else if final_score >= 60 {
818 "D"
819 } else {
820 "F"
821 };
822
823 let risk = if final_score >= 85 {
824 "Low Risk"
825 } else if final_score >= 70 {
826 "Medium Risk"
827 } else if final_score >= 50 {
828 "High Risk"
829 } else {
830 "Critical Risk"
831 };
832
833 SecurityScoreResult {
834 overall_score: final_score,
835 grade: grade.into(),
836 risk_level: risk.into(),
837 score_breakdown: breakdown,
838 }
839}
840
841fn generate_recommendations(
844 headers: &SecurityHeadersResult,
845 ssl: &SslAnalysisResult,
846 waf: &WafDetectionResult,
847 https_available: bool,
848 https_redirect: bool,
849) -> Vec<String> {
850 let mut recs = Vec::new();
851
852 if !headers.missing_critical.is_empty() {
853 recs.push(format!(
854 "CRITICAL: Implement missing security headers: {}",
855 headers.missing_critical.join(", ")
856 ));
857 }
858 if !headers.missing_high.is_empty() {
859 recs.push(format!(
860 "HIGH: Add security headers: {}",
861 headers.missing_high.join(", ")
862 ));
863 }
864
865 match ssl.overall_grade.as_str() {
866 "D" | "F" => recs.push("CRITICAL: Upgrade SSL/TLS configuration".into()),
867 "C" => recs.push("MEDIUM: Consider improving SSL/TLS configuration".into()),
868 _ => {}
869 }
870
871 if !waf.detected {
872 recs.push("MEDIUM: Consider implementing a Web Application Firewall (WAF)".into());
873 }
874
875 if !https_available {
876 recs.push("CRITICAL: Enable HTTPS for secure communication".into());
877 } else if !https_redirect {
878 recs.push("MEDIUM: Implement automatic HTTP to HTTPS redirect".into());
879 }
880
881 recs.truncate(10);
882 recs
883}