Skip to main content

web_analyzer/
security_analysis.rs

1use regex::Regex;
2use reqwest::{Client, Method};
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::time::Duration;
6
7// ── WAF signature database ──────────────────────────────────────────────────
8
9struct 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
53/// Security headers with importance levels
54const 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
64/// Error patterns for vulnerability scanning
65const ERROR_PATTERNS: &[(&str, &str)] = &[
66    ("fatal error", "PHP Fatal Error"),
67    ("warning.*mysql", "MySQL Warning"),
68    ("error.*sql", "SQL Error"),
69];
70
71// ── Data Structures ─────────────────────────────────────────────────────────
72
73#[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
190// ── Main function ───────────────────────────────────────────────────────────
191
192pub async fn analyze_security(
193    domain: &str,
194) -> Result<SecurityAnalysisResult, Box<dyn std::error::Error + Send + Sync>> {
195    let clean = if domain.starts_with("http://") || domain.starts_with("https://") {
196        domain
197            .split("//")
198            .nth(1)
199            .unwrap_or(domain)
200            .split('/')
201            .next()
202            .unwrap_or(domain)
203            .to_string()
204    } else {
205        domain.to_string()
206    };
207
208    let client = Client::builder()
209        .timeout(Duration::from_secs(30))
210        .danger_accept_invalid_certs(true)
211        .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
212        .build()?;
213
214    // ── HTTP + HTTPS requests ───────────────────────────────────────────
215    let http_url = format!("http://{}", clean);
216    let https_url = format!("https://{}", clean);
217
218    // Check HTTPS redirect from HTTP (no-follow)
219    let redir_client = Client::builder()
220        .timeout(Duration::from_secs(15))
221        .danger_accept_invalid_certs(true)
222        .redirect(reqwest::redirect::Policy::none())
223        .user_agent("Mozilla/5.0")
224        .build()?;
225
226    let mut https_redirect = false;
227    if let Ok(resp) = redir_client.get(&http_url).send().await {
228        let status = resp.status().as_u16();
229        if [301, 302, 307, 308].contains(&status) {
230            if let Some(loc) = resp.headers().get("location") {
231                if let Ok(l) = loc.to_str() {
232                    if l.starts_with("https://") {
233                        https_redirect = true;
234                    }
235                }
236            }
237        }
238    }
239
240    // Primary response (prefer HTTPS)
241    let https_resp = client.get(&https_url).send().await;
242    let https_available = https_resp.is_ok();
243
244    let primary = if let Ok(r) = https_resp {
245        r
246    } else {
247        client.get(&http_url).send().await?
248    };
249
250    let resp_url = primary.url().to_string();
251    let headers = primary.headers().clone();
252    let body_text = primary.text().await.unwrap_or_default();
253
254    // ── 1. WAF Detection ────────────────────────────────────────────────
255    let waf_detection = detect_waf(&headers);
256
257    // ── 2. Security Headers ─────────────────────────────────────────────
258    let security_headers = analyze_security_headers(&headers);
259
260    // ── 3. SSL Analysis ─────────────────────────────────────────────────
261    let ssl_analysis = analyze_ssl(&clean).await;
262
263    // ── 4. CORS Policy ──────────────────────────────────────────────────
264    let cors_policy = analyze_cors(&headers);
265
266    // ── 5. Cookie Security ──────────────────────────────────────────────
267    let cookie_security = analyze_cookies(&headers);
268
269    // ── 6. HTTP Methods ─────────────────────────────────────────────────
270    let http_methods = detect_methods(&client, &https_url).await;
271
272    // ── 7. Server Information ───────────────────────────────────────────
273    let server_information = analyze_server_info(&headers);
274
275    // ── 8. Vulnerability Scan ───────────────────────────────────────────
276    let vulnerability_scan = perform_vuln_scan(&resp_url, &body_text);
277
278    // ── 9. Score & Recommendations ──────────────────────────────────────
279    let security_score = calculate_score(
280        &security_headers,
281        &ssl_analysis,
282        &waf_detection,
283        &vulnerability_scan,
284    );
285    let recommendations = generate_recommendations(
286        &security_headers,
287        &ssl_analysis,
288        &waf_detection,
289        https_available,
290        https_redirect,
291    );
292
293    Ok(SecurityAnalysisResult {
294        domain: clean,
295        https_available,
296        https_redirect,
297        waf_detection,
298        security_headers,
299        ssl_analysis,
300        cors_policy,
301        cookie_security,
302        http_methods,
303        server_information,
304        vulnerability_scan,
305        security_score,
306        recommendations,
307    })
308}
309
310// ── WAF Detection ───────────────────────────────────────────────────────────
311
312fn detect_waf(headers: &reqwest::header::HeaderMap) -> WafDetectionResult {
313    let headers_str = format!("{:?}", headers).to_lowercase();
314    let server_header = headers
315        .get("server")
316        .and_then(|v| v.to_str().ok())
317        .unwrap_or("")
318        .to_lowercase();
319
320    let mut detected = Vec::new();
321
322    for sig in WAF_SIGNATURES {
323        let mut confidence: u32 = 0;
324        let mut methods = Vec::new();
325
326        for h in sig.headers {
327            if headers_str.contains(h) {
328                confidence += 40;
329                methods.push(format!("Header: {}", h));
330            }
331        }
332        for s in sig.server {
333            if server_header.contains(s) {
334                confidence += 30;
335                methods.push(format!("Server: {}", s));
336            }
337        }
338
339        if confidence > 0 {
340            let conf_str = if confidence >= 50 {
341                "High"
342            } else if confidence >= 30 {
343                "Medium"
344            } else {
345                "Low"
346            };
347            detected.push(WafMatch {
348                provider: sig.name.to_string(),
349                confidence: conf_str.into(),
350                detection_methods: methods,
351                score: confidence,
352            });
353        }
354    }
355
356    detected.sort_by(|a, b| b.score.cmp(&a.score));
357
358    WafDetectionResult {
359        detected: !detected.is_empty(),
360        primary_waf: detected.first().cloned(),
361        all_detected: detected,
362    }
363}
364
365// ── Security Headers ────────────────────────────────────────────────────────
366
367fn analyze_security_headers(headers: &reqwest::header::HeaderMap) -> SecurityHeadersResult {
368    let mut analysis = HashMap::new();
369    let mut total_score: u32 = 0;
370    let mut max_score: u32 = 0;
371    let mut missing_critical = Vec::new();
372    let mut missing_high = Vec::new();
373
374    for &(name, importance) in SECURITY_HEADERS {
375        let present = headers.get(name).is_some();
376        let value = headers
377            .get(name)
378            .and_then(|v| v.to_str().ok())
379            .unwrap_or("Not Set")
380            .to_string();
381
382        let security_level = if present {
383            "Good".into()
384        } else if importance == "Critical" {
385            "Critical".into()
386        } else {
387            "Medium".into()
388        };
389
390        let weight = match importance {
391            "Critical" => 30,
392            "High" => 20,
393            _ => 10,
394        };
395        max_score += weight;
396        if present {
397            total_score += weight;
398        } else if importance == "Critical" {
399            missing_critical.push(name.to_string());
400        } else if importance == "High" {
401            missing_high.push(name.to_string());
402        }
403
404        analysis.insert(
405            name.to_string(),
406            HeaderAnalysis {
407                present,
408                value,
409                importance: importance.into(),
410                security_level,
411            },
412        );
413    }
414
415    let score = if max_score > 0 {
416        total_score * 100 / max_score
417    } else {
418        0
419    };
420
421    SecurityHeadersResult {
422        headers: analysis,
423        score,
424        missing_critical,
425        missing_high,
426    }
427}
428
429// ── SSL/TLS Analysis ────────────────────────────────────────────────────────
430
431async fn analyze_ssl(domain: &str) -> SslAnalysisResult {
432    let output = match tokio::process::Command::new("openssl")
433        .args([
434            "s_client",
435            "-connect",
436            &format!("{}:443", domain),
437            "-servername",
438            domain,
439        ])
440        .stdin(std::process::Stdio::null())
441        .stdout(std::process::Stdio::piped())
442        .stderr(std::process::Stdio::piped())
443        .output()
444        .await
445    {
446        Ok(o) => String::from_utf8_lossy(&o.stdout).to_string(),
447        Err(_) => {
448            return SslAnalysisResult {
449                ssl_available: false,
450                protocol_version: None,
451                cipher_suite: None,
452                cipher_strength: "Unknown".into(),
453                overall_grade: "F".into(),
454                subject: None,
455                issuer: None,
456            }
457        }
458    };
459
460    if !output.contains("CONNECTED") {
461        return SslAnalysisResult {
462            ssl_available: false,
463            protocol_version: None,
464            cipher_suite: None,
465            cipher_strength: "Unknown".into(),
466            overall_grade: "F".into(),
467            subject: None,
468            issuer: None,
469        };
470    }
471
472    let protocol = Regex::new(r"Protocol\s*:\s*(.+)")
473        .ok()
474        .and_then(|r| r.captures(&output))
475        .and_then(|c| c.get(1).map(|m| m.as_str().trim().to_string()));
476
477    let cipher_suite = Regex::new(r"Cipher\s*:\s*(.+)")
478        .ok()
479        .and_then(|r| r.captures(&output))
480        .and_then(|c| c.get(1).map(|m| m.as_str().trim().to_string()));
481
482    let subject = Regex::new(r"subject=.*?CN\s*=\s*([^\n/,]+)")
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 issuer = Regex::new(r"issuer=.*?CN\s*=\s*([^\n/,]+)")
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    // Cipher strength
493    let cipher_strength = match &cipher_suite {
494        Some(c) if c.contains("AES256") || c.contains("CHACHA20") || c.contains("TLS_AES_256") => {
495            "Strong"
496        }
497        Some(c) if c.contains("AES128") => "Medium",
498        Some(c) if c.contains("DES") || c.contains("RC4") || c.contains("NULL") => "Weak",
499        _ => "Unknown",
500    };
501
502    // Grade
503    let proto_str = protocol.as_deref().unwrap_or("");
504    let grade = if proto_str.contains("TLSv1.3") {
505        "A+"
506    } else if proto_str.contains("TLSv1.2") && cipher_strength == "Strong" {
507        "A"
508    } else if proto_str.contains("TLSv1.2") {
509        "B"
510    } else if proto_str.contains("TLSv1.1") || proto_str.contains("TLSv1") {
511        "C"
512    } else {
513        "F"
514    };
515
516    SslAnalysisResult {
517        ssl_available: true,
518        protocol_version: protocol,
519        cipher_suite,
520        cipher_strength: cipher_strength.into(),
521        overall_grade: grade.into(),
522        subject,
523        issuer,
524    }
525}
526
527// ── CORS Policy ─────────────────────────────────────────────────────────────
528
529fn analyze_cors(headers: &reqwest::header::HeaderMap) -> CorsPolicyResult {
530    let cors_keys = [
531        "access-control-allow-origin",
532        "access-control-allow-methods",
533        "access-control-allow-headers",
534        "access-control-allow-credentials",
535    ];
536
537    let mut cors_headers = HashMap::new();
538    let mut configured = false;
539    let mut issues = Vec::new();
540
541    for &key in &cors_keys {
542        let val = headers
543            .get(key)
544            .and_then(|v| v.to_str().ok())
545            .unwrap_or("Not Set")
546            .to_string();
547        if val != "Not Set" {
548            configured = true;
549        }
550        cors_headers.insert(key.to_string(), val);
551    }
552
553    let origin = cors_headers
554        .get("access-control-allow-origin")
555        .map(|s| s.as_str())
556        .unwrap_or("Not Set");
557    let creds = cors_headers
558        .get("access-control-allow-credentials")
559        .map(|s| s.as_str())
560        .unwrap_or("Not Set");
561
562    if origin == "*" && creds == "true" {
563        issues.push("Critical: Wildcard origin with credentials allowed".into());
564    } else if origin == "*" {
565        issues.push("Warning: Wildcard origin allows all domains".into());
566    }
567
568    let security_level = if issues.is_empty() {
569        "High"
570    } else if issues.len() <= 1 {
571        "Medium"
572    } else {
573        "Low"
574    };
575
576    CorsPolicyResult {
577        configured,
578        headers: cors_headers,
579        issues,
580        security_level: security_level.into(),
581    }
582}
583
584// ── Cookie Security ─────────────────────────────────────────────────────────
585
586fn analyze_cookies(headers: &reqwest::header::HeaderMap) -> CookieSecurityResult {
587    let cookie_val = match headers.get("set-cookie").and_then(|v| v.to_str().ok()) {
588        Some(c) => c.to_string(),
589        None => {
590            return CookieSecurityResult {
591                cookies_present: false,
592                security_issues: vec![],
593                security_score: 100,
594            }
595        }
596    };
597
598    let mut issues = Vec::new();
599    if !cookie_val.contains("Secure") {
600        issues.push("Missing Secure flag".into());
601    }
602    if !cookie_val.contains("HttpOnly") {
603        issues.push("Missing HttpOnly flag".into());
604    }
605    if !cookie_val.contains("SameSite") {
606        issues.push("Missing SameSite attribute".into());
607    }
608
609    let score = 100u32.saturating_sub(issues.len() as u32 * 25);
610
611    CookieSecurityResult {
612        cookies_present: true,
613        security_issues: issues,
614        security_score: score,
615    }
616}
617
618// ── HTTP Methods ────────────────────────────────────────────────────────────
619
620async fn detect_methods(client: &Client, url: &str) -> HttpMethodsResult {
621    let dangerous = ["DELETE", "PUT", "PATCH", "TRACE", "CONNECT"];
622
623    match client.request(Method::OPTIONS, url).send().await {
624        Ok(resp) => {
625            let allow = resp
626                .headers()
627                .get("allow")
628                .and_then(|v| v.to_str().ok())
629                .unwrap_or("");
630            let methods: Vec<String> = allow
631                .split(',')
632                .map(|m| m.trim().to_string())
633                .filter(|m| !m.is_empty())
634                .collect();
635
636            let found_dangerous: Vec<String> = methods
637                .iter()
638                .filter(|m| dangerous.contains(&m.to_uppercase().as_str()))
639                .cloned()
640                .collect();
641
642            let risk = if !found_dangerous.is_empty() {
643                "High"
644            } else {
645                "Low"
646            };
647
648            HttpMethodsResult {
649                methods_detected: true,
650                allowed_methods: methods,
651                dangerous_methods: found_dangerous,
652                security_risk: risk.into(),
653            }
654        }
655        Err(_) => HttpMethodsResult {
656            methods_detected: false,
657            allowed_methods: vec![],
658            dangerous_methods: vec![],
659            security_risk: "Unknown".into(),
660        },
661    }
662}
663
664// ── Server Information ──────────────────────────────────────────────────────
665
666fn analyze_server_info(headers: &reqwest::header::HeaderMap) -> ServerInfoResult {
667    let disclosure_headers = [
668        ("server", "Web server version disclosed"),
669        ("x-powered-by", "Technology stack disclosed"),
670    ];
671
672    let mut server_headers = HashMap::new();
673    let mut issues = Vec::new();
674
675    for &(header, issue) in &disclosure_headers {
676        if let Some(val) = headers.get(header).and_then(|v| v.to_str().ok()) {
677            server_headers.insert(header.to_string(), val.to_string());
678            issues.push(issue.to_string());
679        }
680    }
681
682    let count = issues.len();
683    let level = if count > 2 {
684        "High"
685    } else if count > 0 {
686        "Medium"
687    } else {
688        "Good"
689    };
690
691    ServerInfoResult {
692        server_headers,
693        information_disclosure: issues,
694        disclosure_count: count,
695        security_level: level.into(),
696    }
697}
698
699// ── Vulnerability Scan ──────────────────────────────────────────────────────
700
701fn perform_vuln_scan(resp_url: &str, body: &str) -> VulnScanResult {
702    let mut vulns = Vec::new();
703
704    // HTTPS enforcement
705    if !resp_url.starts_with("https://") {
706        vulns.push(VulnerabilityFound {
707            vuln_type: "Insecure Transport".into(),
708            severity: "High".into(),
709            description: "Site not enforcing HTTPS".into(),
710        });
711    }
712
713    // Error patterns in body
714    for &(pattern, desc) in ERROR_PATTERNS {
715        if let Ok(rx) = Regex::new(&format!("(?i){}", pattern)) {
716            if rx.is_match(body) {
717                vulns.push(VulnerabilityFound {
718                    vuln_type: "Information Disclosure".into(),
719                    severity: "Low".into(),
720                    description: format!("{} detected in response", desc),
721                });
722            }
723        }
724    }
725
726    let risk = calculate_risk_level(&vulns);
727
728    VulnScanResult {
729        vulnerabilities_found: vulns.len(),
730        vulnerabilities: vulns,
731        risk_level: risk,
732    }
733}
734
735fn calculate_risk_level(vulns: &[VulnerabilityFound]) -> String {
736    if vulns.is_empty() {
737        return "Low".into();
738    }
739    let total: u32 = vulns
740        .iter()
741        .map(|v| match v.severity.as_str() {
742            "High" => 3,
743            "Medium" => 2,
744            _ => 1,
745        })
746        .sum();
747
748    if total >= 6 {
749        "Critical".into()
750    } else if total >= 4 {
751        "High".into()
752    } else if total >= 2 {
753        "Medium".into()
754    } else {
755        "Low".into()
756    }
757}
758
759// ── Security Score (weighted composite) ─────────────────────────────────────
760
761fn calculate_score(
762    headers: &SecurityHeadersResult,
763    ssl: &SslAnalysisResult,
764    waf: &WafDetectionResult,
765    vulns: &VulnScanResult,
766) -> SecurityScoreResult {
767    let mut breakdown = HashMap::new();
768    let mut total: f64 = 100.0;
769
770    // Security Headers (40%)
771    let h_score = headers.score;
772    breakdown.insert("security_headers".into(), h_score);
773    total -= (100.0 - h_score as f64) * 0.4;
774
775    // SSL (30%)
776    let ssl_score: u32 = match ssl.overall_grade.as_str() {
777        "A+" => 100,
778        "A" => 90,
779        "B" => 75,
780        "C" => 60,
781        "D" => 40,
782        _ => 0,
783    };
784    breakdown.insert("ssl_tls".into(), ssl_score);
785    total -= (100.0 - ssl_score as f64) * 0.3;
786
787    // WAF (15%)
788    let waf_score: u32 = if waf.detected { 100 } else { 60 };
789    breakdown.insert("waf_protection".into(), waf_score);
790    total -= (100.0 - waf_score as f64) * 0.15;
791
792    // Vulnerabilities (15%)
793    let vuln_score = 100u32.saturating_sub(vulns.vulnerabilities_found as u32 * 20);
794    breakdown.insert("vulnerabilities".into(), vuln_score);
795    total -= (100.0 - vuln_score as f64) * 0.15;
796
797    let final_score = total.clamp(0.0, 100.0) as u32;
798
799    let grade = if final_score >= 95 {
800        "A+"
801    } else if final_score >= 90 {
802        "A"
803    } else if final_score >= 80 {
804        "B"
805    } else if final_score >= 70 {
806        "C"
807    } else if final_score >= 60 {
808        "D"
809    } else {
810        "F"
811    };
812
813    let risk = if final_score >= 85 {
814        "Low Risk"
815    } else if final_score >= 70 {
816        "Medium Risk"
817    } else if final_score >= 50 {
818        "High Risk"
819    } else {
820        "Critical Risk"
821    };
822
823    SecurityScoreResult {
824        overall_score: final_score,
825        grade: grade.into(),
826        risk_level: risk.into(),
827        score_breakdown: breakdown,
828    }
829}
830
831// ── Recommendations ─────────────────────────────────────────────────────────
832
833fn generate_recommendations(
834    headers: &SecurityHeadersResult,
835    ssl: &SslAnalysisResult,
836    waf: &WafDetectionResult,
837    https_available: bool,
838    https_redirect: bool,
839) -> Vec<String> {
840    let mut recs = Vec::new();
841
842    if !headers.missing_critical.is_empty() {
843        recs.push(format!(
844            "CRITICAL: Implement missing security headers: {}",
845            headers.missing_critical.join(", ")
846        ));
847    }
848    if !headers.missing_high.is_empty() {
849        recs.push(format!(
850            "HIGH: Add security headers: {}",
851            headers.missing_high.join(", ")
852        ));
853    }
854
855    match ssl.overall_grade.as_str() {
856        "D" | "F" => recs.push("CRITICAL: Upgrade SSL/TLS configuration".into()),
857        "C" => recs.push("MEDIUM: Consider improving SSL/TLS configuration".into()),
858        _ => {}
859    }
860
861    if !waf.detected {
862        recs.push("MEDIUM: Consider implementing a Web Application Firewall (WAF)".into());
863    }
864
865    if !https_available {
866        recs.push("CRITICAL: Enable HTTPS for secure communication".into());
867    } else if !https_redirect {
868        recs.push("MEDIUM: Implement automatic HTTP to HTTPS redirect".into());
869    }
870
871    recs.truncate(10);
872    recs
873}