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 = 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    // ── 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 = 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    // 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 {
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    // ── 2. Security Headers ─────────────────────────────────────────────
269    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    // ── 3. SSL Analysis ─────────────────────────────────────────────────
282    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    // ── 4. CORS Policy ──────────────────────────────────────────────────
295    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    // ── 5. Cookie Security ──────────────────────────────────────────────
308    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    // ── 6. HTTP Methods ─────────────────────────────────────────────────
321    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    // ── 7. Server Information ───────────────────────────────────────────
334    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    // ── 8. Vulnerability Scan ───────────────────────────────────────────
347    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    // ── 9. Score & Recommendations ──────────────────────────────────────
360    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
401// ── WAF Detection ───────────────────────────────────────────────────────────
402
403fn 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
456// ── Security Headers ────────────────────────────────────────────────────────
457
458fn 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
519// ── SSL/TLS Analysis ────────────────────────────────────────────────────────
520
521async 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    // Cipher strength
583    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    // Grade
593    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
617// ── CORS Policy ─────────────────────────────────────────────────────────────
618
619fn 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
674// ── Cookie Security ─────────────────────────────────────────────────────────
675
676fn 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
708// ── HTTP Methods ────────────────────────────────────────────────────────────
709
710async 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
754// ── Server Information ──────────────────────────────────────────────────────
755
756fn 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
789// ── Vulnerability Scan ──────────────────────────────────────────────────────
790
791fn perform_vuln_scan(resp_url: &str, body: &str) -> VulnScanResult {
792    let mut vulns = Vec::new();
793
794    // HTTPS enforcement
795    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    // Error patterns in body
804    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
849// ── Security Score (weighted composite) ─────────────────────────────────────
850
851fn 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    // Security Headers (40%)
861    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    // SSL (30%)
866    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    // WAF (15%)
878    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    // Vulnerabilities (15%)
883    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
921// ── Recommendations ─────────────────────────────────────────────────────────
922
923fn 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}