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    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    // ── HTTP + HTTPS requests ───────────────────────────────────────────
216    let http_url = format!("http://{}", clean);
217    let https_url = format!("https://{}", clean);
218
219    // Check HTTPS redirect from HTTP (no-follow)
220    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    // Primary response (prefer HTTPS)
242    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    // ── 1. WAF Detection ────────────────────────────────────────────────
256    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    // ── 2. Security Headers ─────────────────────────────────────────────
260    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    // ── 3. SSL Analysis ─────────────────────────────────────────────────
264    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    // ── 4. CORS Policy ──────────────────────────────────────────────────
268    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    // ── 5. Cookie Security ──────────────────────────────────────────────
272    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    // ── 6. HTTP Methods ─────────────────────────────────────────────────
276    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    // ── 7. Server Information ───────────────────────────────────────────
280    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    // ── 8. Vulnerability Scan ───────────────────────────────────────────
284    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    // ── 9. Score & Recommendations ──────────────────────────────────────
288    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
320// ── WAF Detection ───────────────────────────────────────────────────────────
321
322fn 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
375// ── Security Headers ────────────────────────────────────────────────────────
376
377fn 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
439// ── SSL/TLS Analysis ────────────────────────────────────────────────────────
440
441async 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    // Cipher strength
503    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    // Grade
513    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
537// ── CORS Policy ─────────────────────────────────────────────────────────────
538
539fn 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
594// ── Cookie Security ─────────────────────────────────────────────────────────
595
596fn 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
628// ── HTTP Methods ────────────────────────────────────────────────────────────
629
630async 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
674// ── Server Information ──────────────────────────────────────────────────────
675
676fn 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
709// ── Vulnerability Scan ──────────────────────────────────────────────────────
710
711fn perform_vuln_scan(resp_url: &str, body: &str) -> VulnScanResult {
712    let mut vulns = Vec::new();
713
714    // HTTPS enforcement
715    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    // Error patterns in body
724    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
769// ── Security Score (weighted composite) ─────────────────────────────────────
770
771fn 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    // Security Headers (40%)
781    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    // SSL (30%)
786    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    // WAF (15%)
798    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    // Vulnerabilities (15%)
803    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
841// ── Recommendations ─────────────────────────────────────────────────────────
842
843fn 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}