Skip to main content

web_analyzer/
web_technologies.rs

1use regex::Regex;
2use reqwest::Client;
3use scraper::{Html, Selector};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::time::Duration;
7
8// ── Detection pattern constants ─────────────────────────────────────────────
9
10const WEB_SERVERS: &[(&str, &str)] = &[
11    ("nginx", "Nginx"),
12    ("apache", "Apache HTTP Server"),
13    ("iis", "Microsoft IIS"),
14    ("cloudflare", "Cloudflare"),
15    ("litespeed", "LiteSpeed"),
16    ("caddy", "Caddy"),
17    ("traefik", "Traefik Proxy"),
18    ("envoy", "Envoy Proxy"),
19    ("gunicorn", "Gunicorn WSGI"),
20    ("uwsgi", "uWSGI"),
21];
22
23const JS_LIBRARIES: &[(&str, &[&str])] = &[
24    ("jQuery", &["jquery", "jquery.min.js"]),
25    ("Lodash", &["lodash", "underscore"]),
26    ("Moment.js", &["moment.js", "moment.min.js"]),
27    ("D3.js", &["d3.js", "d3.min.js"]),
28    ("Chart.js", &["chart.js", "chart.min.js"]),
29    ("Three.js", &["three.js", "three.min.js"]),
30    ("GSAP", &["gsap", "tweenmax"]),
31    ("Axios", &["axios"]),
32    ("Swiper", &["swiper"]),
33    ("Bootstrap JS", &["bootstrap.js", "bootstrap.min.js"]),
34    ("Popper.js", &["popper.js"]),
35    ("Font Awesome", &["fontawesome", "font-awesome"]),
36];
37
38const CSS_FRAMEWORKS: &[(&str, &[&str])] = &[
39    ("Bootstrap", &["bootstrap"]),
40    ("Tailwind CSS", &["tailwind"]),
41    ("Bulma", &["bulma"]),
42    ("Foundation", &["foundation"]),
43    ("Semantic UI", &["semantic-ui"]),
44    ("Materialize", &["materialize"]),
45    ("UIKit", &["uikit"]),
46    ("Pure CSS", &["pure-css", "pure-"]),
47];
48
49const CMS_PATTERNS: &[(&str, &[&str])] = &[
50    (
51        "WordPress",
52        &["wp-content", "wp-includes", "wp-admin", "wordpress"],
53    ),
54    ("Drupal", &["drupal", "sites/all", "sites/default"]),
55    ("Joomla", &["joomla", "option=com_"]),
56    ("Magento", &["magento", "mage/cookies.js", "skin/frontend"]),
57    ("Shopify", &["shopify", "shopifycdn"]),
58    ("Wix", &["wix.com", "wixstatic"]),
59    ("Squarespace", &["squarespace", "sqsp"]),
60    ("Ghost", &["ghost.io", "casper"]),
61    ("Webflow", &["webflow"]),
62    ("TYPO3", &["typo3", "typo3conf"]),
63    ("Concrete5", &["concrete5"]),
64];
65
66const ECOMMERCE: &[(&str, &[&str])] = &[
67    ("Shopify", &["shopify", "shopifycdn"]),
68    ("WooCommerce", &["woocommerce", "wc-"]),
69    ("Magento", &["magento", "mage"]),
70    ("PrestaShop", &["prestashop"]),
71    ("BigCommerce", &["bigcommerce"]),
72    ("OpenCart", &["opencart"]),
73    ("Stripe", &["stripe"]),
74    ("PayPal", &["paypal"]),
75    ("Square", &["squareup"]),
76];
77
78const ANALYTICS: &[(&str, &[&str])] = &[
79    (
80        "Google Analytics",
81        &["google-analytics", "googletagmanager", "gtag"],
82    ),
83    ("Google Tag Manager", &["googletagmanager"]),
84    ("Facebook Pixel", &["facebook.net/tr", "fbevents.js"]),
85    ("Hotjar", &["hotjar"]),
86    ("Mixpanel", &["mixpanel"]),
87    ("Segment", &["segment.com", "analytics.js"]),
88    ("Adobe Analytics", &["adobe", "omniture"]),
89    ("Yandex Metrica", &["yandex", "metrica"]),
90];
91
92const WAF_INDICATORS: &[(&str, &[&str])] = &[
93    ("Cloudflare", &["cf-ray", "cloudflare"]),
94    ("AWS WAF", &["x-amzn-requestid", "awselb"]),
95    ("Incapsula", &["incap_ses", "incapsula"]),
96    ("Akamai", &["akamai"]),
97    ("Sucuri", &["sucuri"]),
98    ("ModSecurity", &["mod_security"]),
99    ("F5 BIG-IP", &["bigip", "f5"]),
100    ("Barracuda", &["barracuda"]),
101];
102
103const SECURITY_HEADERS: &[(&str, &str)] = &[
104    ("Content-Security-Policy", "High"),
105    ("Strict-Transport-Security", "High"),
106    ("X-Frame-Options", "Medium"),
107    ("X-Content-Type-Options", "Medium"),
108    ("X-XSS-Protection", "Medium"),
109    ("Referrer-Policy", "Medium"),
110];
111
112const WP_KNOWN_PLUGINS: &[(&str, &str)] = &[
113    ("yoast", "Yoast SEO"),
114    ("akismet", "Akismet Anti-Spam"),
115    ("jetpack", "Jetpack"),
116    ("woocommerce", "WooCommerce"),
117    ("contact-form-7", "Contact Form 7"),
118    ("elementor", "Elementor"),
119    ("wordfence", "Wordfence Security"),
120    ("wp-super-cache", "WP Super Cache"),
121    ("all-in-one-seo", "All in One SEO"),
122    ("google-analytics", "Google Analytics"),
123];
124
125// ── Data Structures ─────────────────────────────────────────────────────────
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct WebTechResult {
129    pub domain: String,
130    // Basic technology
131    pub web_server: String,
132    pub backend: Vec<String>,
133    pub frontend: Vec<String>,
134    pub js_libraries: Vec<String>,
135    pub css_frameworks: Vec<String>,
136    pub cms: Vec<String>,
137    pub ecommerce: Vec<String>,
138    pub cdn: Vec<String>,
139    pub analytics: Vec<String>,
140    // Security
141    pub security_headers: HashMap<String, SecurityHeaderInfo>,
142    pub security_vulnerabilities: VulnerabilityInfo,
143    pub information_disclosure: DisclosureInfo,
144    pub security_services: SecurityServicesInfo,
145    pub cookie_security: CookieSecurityInfo,
146    // WordPress
147    pub is_wordpress: bool,
148    pub wordpress_analysis: Option<WordPressAnalysis>,
149    // Score
150    pub security_score: SecurityScoreResult,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct SecurityHeaderInfo {
155    pub present: bool,
156    pub value: String,
157    pub security_level: String,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct VulnerabilityInfo {
162    pub missing_security_headers: Vec<String>,
163    pub insecure_practices: Vec<String>,
164    pub exposed_information: Vec<String>,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct DisclosureInfo {
169    pub server_info: Vec<String>,
170    pub technology_disclosure: Vec<String>,
171    pub file_exposure: Vec<String>,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct SecurityServicesInfo {
176    pub waf: Vec<String>,
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct CookieSecurityInfo {
181    pub secure_flag: bool,
182    pub httponly_flag: bool,
183    pub samesite_attribute: bool,
184    pub security_score: u32,
185    pub security_level: String,
186    pub recommendations: Vec<String>,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct WordPressAnalysis {
191    pub confidence: String,
192    pub version: String,
193    pub theme: String,
194    pub plugins: Vec<String>,
195    pub users_found: Vec<WpUser>,
196    pub rest_api_enabled: bool,
197    pub xmlrpc_enabled: bool,
198    pub admin_accessible: bool,
199    pub login_accessible: bool,
200    pub debug_enabled: bool,
201    pub security_issues: Vec<String>,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct WpUser {
206    pub id: u64,
207    pub username: String,
208    pub display_name: String,
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct SecurityScoreResult {
213    pub overall_score: u32,
214    pub security_grade: String,
215    pub risk_level: String,
216    pub critical_issues: Vec<String>,
217    pub recommendations: Vec<String>,
218}
219
220// ── Main Function ───────────────────────────────────────────────────────────
221
222pub async fn detect_web_technologies(
223    domain: &str,
224    progress_tx: Option<tokio::sync::mpsc::Sender<crate::ScanProgress>>,
225) -> Result<WebTechResult, Box<dyn std::error::Error + Send + Sync>> {
226    report_progress(
227        &progress_tx,
228        5.0,
229        format!("Preparing technology fingerprint for {}", domain),
230        "Info",
231    );
232
233    let url = if domain.starts_with("http") {
234        domain.to_string()
235    } else {
236        format!("https://{}", domain)
237    };
238
239    let client = crate::http_client_builder()
240        .timeout(Duration::from_secs(30))
241        .danger_accept_invalid_certs(true)
242        .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
243        .build()?;
244
245    report_progress(&progress_tx, 15.0, format!("Fetching {}", url), "Info");
246    let res = match client.get(&url).send().await {
247        Ok(res) => res,
248        Err(e) => {
249            report_progress(
250                &progress_tx,
251                100.0,
252                format!("Failed to fetch target: {}", e),
253                "Error",
254            );
255            return Err(Box::new(e));
256        }
257    };
258    let headers = res.headers().clone();
259    report_progress(
260        &progress_tx,
261        30.0,
262        "Response received; reading HTML",
263        "Success",
264    );
265    let html_raw = match res.text().await {
266        Ok(html) => html,
267        Err(e) => {
268            report_progress(
269                &progress_tx,
270                100.0,
271                format!("Failed to read response body: {}", e),
272                "Error",
273            );
274            return Err(Box::new(e));
275        }
276    };
277    let html_lower = html_raw.to_lowercase();
278
279    let base_domain = domain.replace("https://", "").replace("http://", "");
280
281    // Cache header strings
282    let server_hdr = get_header(&headers, "server");
283    let powered_by = get_header(&headers, "x-powered-by").to_lowercase();
284    let headers_str = format!("{:?}", headers).to_lowercase();
285
286    report_progress(
287        &progress_tx,
288        45.0,
289        "Parsing headers and HTML technology signatures",
290        "Info",
291    );
292
293    // ── All Html-dependent (sync) work in a block so it drops before .await ──
294    let (
295        web_server,
296        backend,
297        frontend,
298        js_libraries,
299        css_frameworks,
300        cms,
301        ecommerce,
302        cdn,
303        analytics,
304        security_headers,
305        security_vulnerabilities,
306        information_disclosure,
307        security_services,
308        cookie_security,
309        is_wordpress,
310        wp_version,
311        wp_theme,
312        wp_plugins,
313    ) = {
314        let document = Html::parse_document(&html_raw);
315
316        let web_server = detect_server(&server_hdr, &powered_by);
317        let backend = detect_backend(&html_lower, &powered_by, &server_hdr);
318        let frontend = detect_frontend(&html_lower, &document);
319        let js_libraries = detect_pattern_list(&html_lower, &document, JS_LIBRARIES);
320        let css_frameworks = detect_css(&html_lower, &document);
321        let cms = detect_by_content(&html_lower, CMS_PATTERNS);
322        let ecommerce = detect_by_content(&html_lower, ECOMMERCE);
323        let cdn = detect_cdn(&server_hdr, &headers, &html_lower);
324        let analytics = detect_pattern_list(&html_lower, &document, ANALYTICS);
325        let security_headers = analyze_security_headers(&headers);
326        let security_vulnerabilities = detect_vulnerabilities(&html_lower, &headers);
327        let information_disclosure = detect_disclosure(&html_lower, &server_hdr, &powered_by);
328        let security_services = detect_waf(&headers_str, &html_lower);
329        let cookie_security = analyze_cookies(&headers);
330        let is_wordpress = is_wp(&html_lower);
331
332        // Extract WP data synchronously while we have Html
333        let wp_version = if is_wordpress {
334            extract_wp_version(&html_lower, &document)
335        } else {
336            String::new()
337        };
338        let wp_theme = if is_wordpress {
339            extract_wp_theme(&document)
340        } else {
341            String::new()
342        };
343        let wp_plugins = if is_wordpress {
344            extract_wp_plugins(&html_lower, &document)
345        } else {
346            vec![]
347        };
348
349        (
350            web_server,
351            backend,
352            frontend,
353            js_libraries,
354            css_frameworks,
355            cms,
356            ecommerce,
357            cdn,
358            analytics,
359            security_headers,
360            security_vulnerabilities,
361            information_disclosure,
362            security_services,
363            cookie_security,
364            is_wordpress,
365            wp_version,
366            wp_theme,
367            wp_plugins,
368        )
369    }; // `document` (Html) is dropped here — safe for async after this point
370
371    report_progress(
372        &progress_tx,
373        75.0,
374        "Core technology fingerprinting complete",
375        "Success",
376    );
377
378    // 15. WordPress Analysis (async — no Html involved)
379    let wordpress_analysis = if is_wordpress {
380        report_progress(
381            &progress_tx,
382            85.0,
383            "Running WordPress-specific checks",
384            "Info",
385        );
386        Some(
387            analyze_wordpress(
388                &client,
389                &base_domain,
390                &html_lower,
391                wp_version,
392                wp_theme,
393                wp_plugins,
394            )
395            .await,
396        )
397    } else {
398        None
399    };
400
401    // 16. Security Score
402    let security_score = calculate_score(
403        &security_headers,
404        &security_vulnerabilities,
405        &information_disclosure,
406        &security_services,
407        &cookie_security,
408        &wordpress_analysis,
409    );
410
411    report_progress(
412        &progress_tx,
413        100.0,
414        "Technology fingerprint complete",
415        "Success",
416    );
417
418    Ok(WebTechResult {
419        domain: domain.to_string(),
420        web_server,
421        backend,
422        frontend,
423        js_libraries,
424        css_frameworks,
425        cms,
426        ecommerce,
427        cdn,
428        analytics,
429        security_headers,
430        security_vulnerabilities,
431        information_disclosure,
432        security_services,
433        cookie_security,
434        is_wordpress,
435        wordpress_analysis,
436        security_score,
437    })
438}
439
440fn report_progress(
441    progress_tx: &Option<tokio::sync::mpsc::Sender<crate::ScanProgress>>,
442    percentage: f32,
443    message: impl Into<String>,
444    status: &str,
445) {
446    if let Some(tx) = progress_tx {
447        let _ = tx.try_send(crate::ScanProgress {
448            module: "Web Technologies".into(),
449            percentage,
450            message: message.into(),
451            status: status.into(),
452        });
453    }
454}
455
456// ── 1. Web Server ───────────────────────────────────────────────────────────
457
458fn detect_server(server: &str, powered_by: &str) -> String {
459    let s_lower = server.to_lowercase();
460    let p_lower = powered_by.to_lowercase();
461    let version_re = Regex::new(r"[\d\.]+").ok();
462    for &(key, name) in WEB_SERVERS {
463        if s_lower.contains(key) || p_lower.contains(key) {
464            let version = version_re
465                .as_ref()
466                .and_then(|r| r.find(&s_lower).map(|m| format!(" {}", m.as_str())))
467                .unwrap_or_default();
468            return format!("{}{}", name, version);
469        }
470    }
471    if server.is_empty() {
472        "Not Detected".into()
473    } else {
474        server.to_string()
475    }
476}
477
478// ── 2. Backend Technologies ─────────────────────────────────────────────────
479
480fn detect_backend(html: &str, powered_by: &str, server: &str) -> Vec<String> {
481    let mut techs = vec![];
482    let srv = server.to_lowercase();
483
484    if powered_by.contains("php") || html.contains(".php") || html.contains("phpsessid") {
485        techs.push("PHP".into());
486    }
487    if powered_by.contains("asp.net") || html.contains("__viewstate") || html.contains("aspxauth") {
488        techs.push("ASP.NET".into());
489    }
490    if powered_by.contains("express") || srv.contains("node") || powered_by.contains("koa") {
491        techs.push("Node.js".into());
492    }
493    if html.contains("django") || html.contains("csrfmiddlewaretoken") {
494        techs.push("Python Django".into());
495    }
496    if html.contains("flask") || srv.contains("werkzeug") {
497        techs.push("Python Flask".into());
498    }
499    if powered_by.contains("ruby") || html.contains("rails") || html.contains("authenticity_token")
500    {
501        techs.push("Ruby on Rails".into());
502    }
503    if html.contains("jsessionid")
504        || html.contains("servlet")
505        || html.contains(".jsp")
506        || html.contains("spring")
507    {
508        techs.push("Java".into());
509    }
510    if html.contains("golang") || html.contains("gin-gonic") {
511        techs.push("Go".into());
512    }
513
514    if techs.is_empty() {
515        vec!["Not Detected".into()]
516    } else {
517        techs
518    }
519}
520
521// ── 3. Frontend Technologies ────────────────────────────────────────────────
522
523fn detect_frontend(html: &str, doc: &Html) -> Vec<String> {
524    let mut techs = vec![];
525    let scripts = collect_script_srcs(doc);
526
527    if scripts.contains("react") || html.contains("data-reactroot") || html.contains("__react") {
528        techs.push("React".into());
529    }
530    if scripts.contains("vue") || html.contains("v-app") || html.contains("v-cloak") {
531        techs.push("Vue.js".into());
532    }
533    if scripts.contains("angular") || html.contains("ng-app") || html.contains("ng-version") {
534        techs.push("Angular".into());
535    }
536    if scripts.contains("svelte") || html.contains("_svelte") {
537        techs.push("Svelte".into());
538    }
539    if scripts.contains("ember") || html.contains("ember-application") {
540        techs.push("Ember.js".into());
541    }
542    if scripts.contains("alpine") || html.contains("x-data") {
543        techs.push("Alpine.js".into());
544    }
545    if scripts.contains("jquery") {
546        techs.push("jQuery".into());
547    }
548
549    if techs.is_empty() {
550        vec!["Not Detected".into()]
551    } else {
552        techs
553    }
554}
555
556// ── 4/9. Pattern-based detection (JS libs, Analytics) ───────────────────────
557
558fn detect_pattern_list(html: &str, doc: &Html, patterns: &[(&str, &[&str])]) -> Vec<String> {
559    let mut found = vec![];
560    let scripts = collect_script_srcs(doc);
561    for &(name, pats) in patterns {
562        if pats.iter().any(|p| scripts.contains(p) || html.contains(p)) {
563            found.push(name.to_string());
564        }
565    }
566    if found.is_empty() {
567        vec!["Not Detected".into()]
568    } else {
569        found
570    }
571}
572
573// ── 5. CSS Frameworks ───────────────────────────────────────────────────────
574
575fn detect_css(html: &str, doc: &Html) -> Vec<String> {
576    let mut found = vec![];
577    let stylesheets = collect_stylesheet_hrefs(doc);
578    let combined = format!("{} {}", stylesheets, html);
579    for &(name, pats) in CSS_FRAMEWORKS {
580        if pats.iter().any(|p| combined.contains(p)) {
581            found.push(name.to_string());
582        }
583    }
584    if found.is_empty() {
585        vec!["Not Detected".into()]
586    } else {
587        found
588    }
589}
590
591// ── 6/7. Content-based detection (CMS, E-commerce) ─────────────────────────
592
593fn detect_by_content(html: &str, patterns: &[(&str, &[&str])]) -> Vec<String> {
594    let mut found = vec![];
595    for &(name, pats) in patterns {
596        if pats.iter().any(|p| html.contains(p)) {
597            found.push(name.to_string());
598        }
599    }
600    if found.is_empty() {
601        vec!["Not Detected".into()]
602    } else {
603        found
604    }
605}
606
607// ── 8. CDN ──────────────────────────────────────────────────────────────────
608
609fn detect_cdn(server: &str, headers: &reqwest::header::HeaderMap, html: &str) -> Vec<String> {
610    let mut found = vec![];
611    let s = server.to_lowercase();
612    let via = get_header(headers, "via").to_lowercase();
613
614    if s.contains("cloudflare") || headers.contains_key("cf-ray") {
615        found.push("Cloudflare".into());
616    }
617    if s.contains("cloudfront") || via.contains("cloudfront") || headers.contains_key("x-amz-cf-id")
618    {
619        found.push("AWS CloudFront".into());
620    }
621    if s.contains("fastly") || via.contains("fastly") {
622        found.push("Fastly".into());
623    }
624    if s.contains("keycdn") {
625        found.push("KeyCDN".into());
626    }
627    if html.contains("maxcdn") {
628        found.push("MaxCDN".into());
629    }
630    if s.contains("akamai") || headers.contains_key("x-akamai-transformed") {
631        found.push("Akamai".into());
632    }
633
634    if found.is_empty() {
635        vec!["Not Detected".into()]
636    } else {
637        found
638    }
639}
640
641// ── 10. Security Headers ────────────────────────────────────────────────────
642
643fn analyze_security_headers(
644    headers: &reqwest::header::HeaderMap,
645) -> HashMap<String, SecurityHeaderInfo> {
646    let mut result = HashMap::new();
647    for &(name, importance) in SECURITY_HEADERS {
648        let present = headers.contains_key(name);
649        let value = headers
650            .get(name)
651            .and_then(|v| v.to_str().ok())
652            .unwrap_or("Not Set")
653            .to_string();
654        result.insert(
655            name.to_string(),
656            SecurityHeaderInfo {
657                present,
658                value,
659                security_level: if present {
660                    importance.to_string()
661                } else {
662                    "Low".into()
663                },
664            },
665        );
666    }
667    result
668}
669
670// ── 11. Security Vulnerabilities ────────────────────────────────────────────
671
672fn detect_vulnerabilities(html: &str, headers: &reqwest::header::HeaderMap) -> VulnerabilityInfo {
673    let mut missing = vec![];
674    let required = [
675        ("Content-Security-Policy", "CSP Header Missing - XSS Risk"),
676        ("X-Frame-Options", "Clickjacking Protection Missing"),
677        ("X-Content-Type-Options", "MIME Sniffing Protection Missing"),
678        ("Strict-Transport-Security", "HSTS Missing - MITM Risk"),
679        ("X-XSS-Protection", "XSS Protection Header Missing"),
680    ];
681    for &(header, risk) in &required {
682        if !headers.contains_key(header) {
683            missing.push(risk.to_string());
684        }
685    }
686
687    let mut insecure = vec![];
688    if html.contains("http://") && html.contains("https://") {
689        insecure.push("Mixed Content - HTTP resources on HTTPS page".into());
690    }
691
692    let mut exposed = vec![];
693    let debug_patterns = [
694        (r"debug.*true", "Debug mode enabled"),
695        (r"error.*trace", "Error traces exposed"),
696        (r"stack.*trace", "Stack traces visible"),
697        (r"sql.*error", "SQL errors exposed"),
698    ];
699    for &(pattern, desc) in &debug_patterns {
700        if Regex::new(pattern)
701            .ok()
702            .map(|r| r.is_match(html))
703            .unwrap_or(false)
704        {
705            exposed.push(desc.to_string());
706        }
707    }
708
709    VulnerabilityInfo {
710        missing_security_headers: missing,
711        insecure_practices: insecure,
712        exposed_information: exposed,
713    }
714}
715
716// ── 12. Information Disclosure ───────────────────────────────────────────────
717
718fn detect_disclosure(html: &str, server: &str, powered_by: &str) -> DisclosureInfo {
719    let mut server_info = vec![];
720    if Regex::new(r"/[\d\.]+")
721        .ok()
722        .map(|r| r.is_match(server))
723        .unwrap_or(false)
724    {
725        server_info.push(format!("Server version exposed: {}", server));
726    }
727
728    let mut tech = vec![];
729    if !powered_by.is_empty() {
730        tech.push(format!("Technology stack exposed: {}", powered_by));
731    }
732
733    let mut files = vec![];
734    if html.contains("c:\\") || html.contains("c:/") {
735        files.push("Windows file paths exposed".into());
736    }
737    if html.contains("/var/www/") {
738        files.push("Linux file paths exposed".into());
739    }
740    if html.contains("/home/") {
741        files.push("User directories exposed".into());
742    }
743    if html.contains(".env") {
744        files.push("Environment files referenced".into());
745    }
746
747    DisclosureInfo {
748        server_info,
749        technology_disclosure: tech,
750        file_exposure: files,
751    }
752}
753
754// ── 13. Security Services (WAF) ─────────────────────────────────────────────
755
756fn detect_waf(headers_str: &str, html: &str) -> SecurityServicesInfo {
757    let mut waf = vec![];
758    for &(name, indicators) in WAF_INDICATORS {
759        if indicators
760            .iter()
761            .any(|i| headers_str.contains(i) || html.contains(i))
762        {
763            waf.push(name.to_string());
764        }
765    }
766    SecurityServicesInfo { waf }
767}
768
769// ── 14. Cookie Security ─────────────────────────────────────────────────────
770
771fn analyze_cookies(headers: &reqwest::header::HeaderMap) -> CookieSecurityInfo {
772    let cookie_str = headers
773        .get("set-cookie")
774        .and_then(|v| v.to_str().ok())
775        .unwrap_or("");
776
777    if cookie_str.is_empty() {
778        return CookieSecurityInfo {
779            secure_flag: false,
780            httponly_flag: false,
781            samesite_attribute: false,
782            security_score: 0,
783            security_level: "N/A".into(),
784            recommendations: vec!["No cookies detected".into()],
785        };
786    }
787
788    let secure = cookie_str.to_lowercase().contains("secure");
789    let httponly = cookie_str.to_lowercase().contains("httponly");
790    let samesite = cookie_str.to_lowercase().contains("samesite");
791
792    let mut score = 0u32;
793    let mut recs = vec![];
794    if secure {
795        score += 40;
796    } else {
797        recs.push("Add Secure flag to cookies".into());
798    }
799    if httponly {
800        score += 30;
801    } else {
802        recs.push("Add HttpOnly flag to prevent XSS".into());
803    }
804    if samesite {
805        score += 30;
806    } else {
807        recs.push("Add SameSite attribute for CSRF protection".into());
808    }
809
810    let level = if score >= 90 {
811        "Excellent"
812    } else if score >= 70 {
813        "Good"
814    } else if score >= 50 {
815        "Fair"
816    } else {
817        "Poor"
818    };
819
820    CookieSecurityInfo {
821        secure_flag: secure,
822        httponly_flag: httponly,
823        samesite_attribute: samesite,
824        security_score: score,
825        security_level: level.into(),
826        recommendations: recs,
827    }
828}
829
830// ── 15. WordPress Analysis ──────────────────────────────────────────────────
831
832fn is_wp(html: &str) -> bool {
833    let indicators = [
834        html.contains("wp-content/"),
835        html.contains("wp-includes/"),
836        html.contains("wp-admin/"),
837        html.contains("wp-json/"),
838        html.contains("xmlrpc.php"),
839    ];
840    indicators.iter().filter(|&&x| x).count() >= 2
841}
842
843async fn analyze_wordpress(
844    client: &Client,
845    domain: &str,
846    html: &str,
847    version: String,
848    theme: String,
849    plugins: Vec<String>,
850) -> WordPressAnalysis {
851    let base_url = format!("https://{}", domain);
852
853    // Confidence
854    let confidence = if html.contains("wp-content/") && html.contains("wp-includes/") {
855        "High"
856    } else {
857        "Medium"
858    };
859
860    // Users via REST API
861    let users_found = enumerate_wp_users(client, &base_url).await;
862
863    // REST API
864    let rest_api = check_wp_endpoint(client, &format!("{}/wp-json/", base_url)).await;
865
866    // XMLRPC
867    let xmlrpc = check_wp_xmlrpc(client, &base_url).await;
868
869    // Admin / Login
870    let admin = check_wp_endpoint(client, &format!("{}/wp-admin/", base_url)).await;
871    let login = check_wp_endpoint(client, &format!("{}/wp-login.php", base_url)).await;
872
873    // Debug
874    let debug = html.contains("wp_debug")
875        || Regex::new(r"fatal error.*wp-")
876            .ok()
877            .map(|r| r.is_match(html))
878            .unwrap_or(false);
879
880    // Security issues
881    let mut issues = vec![];
882    if rest_api {
883        issues.push("REST API enabled - user enumeration possible".into());
884    }
885    if xmlrpc {
886        issues.push("XML-RPC enabled - brute force risk".into());
887    }
888    if debug {
889        issues.push("Debug information potentially exposed".into());
890    }
891    if !users_found.is_empty() {
892        issues.push(format!(
893            "{} users enumerated via REST API",
894            users_found.len()
895        ));
896    }
897
898    WordPressAnalysis {
899        confidence: confidence.into(),
900        version,
901        theme,
902        plugins,
903        users_found,
904        rest_api_enabled: rest_api,
905        xmlrpc_enabled: xmlrpc,
906        admin_accessible: admin,
907        login_accessible: login,
908        debug_enabled: debug,
909        security_issues: issues,
910    }
911}
912
913fn extract_wp_version(html: &str, doc: &Html) -> String {
914    // Check generator meta
915    if let Ok(sel) = Selector::parse("meta[name=\"generator\"]") {
916        if let Some(el) = doc.select(&sel).next() {
917            if let Some(content) = el.value().attr("content") {
918                if content.to_lowercase().contains("wordpress") {
919                    if let Some(m) = Regex::new(r"(?i)wordpress\s+([\d\.]+)")
920                        .ok()
921                        .and_then(|r| r.captures(content))
922                    {
923                        return m.get(1).unwrap().as_str().to_string();
924                    }
925                }
926            }
927        }
928    }
929    // Regex on HTML
930    if let Some(m) = Regex::new(r#"ver=([\d\.]+)"#)
931        .ok()
932        .and_then(|r| r.captures(html))
933    {
934        return m.get(1).unwrap().as_str().to_string();
935    }
936    "Unknown".into()
937}
938
939fn extract_wp_theme(doc: &Html) -> String {
940    let theme_re = Regex::new(r"/wp-content/themes/([^/]+)").ok();
941    if let Ok(sel) = Selector::parse("link[rel=\"stylesheet\"]") {
942        for el in doc.select(&sel) {
943            if let Some(href) = el.value().attr("href") {
944                if href.contains("wp-content/themes/") {
945                    if let Some(m) = theme_re.as_ref().and_then(|r| r.captures(href)) {
946                        return m.get(1).unwrap().as_str().to_string();
947                    }
948                }
949            }
950        }
951    }
952    "Unknown".into()
953}
954
955fn extract_wp_plugins(html: &str, doc: &Html) -> Vec<String> {
956    let mut plugins = std::collections::HashSet::new();
957    let plugin_re = Regex::new(r"/wp-content/plugins/([^/]+)").ok();
958
959    // From script/link srcs
960    let selectors = ["script[src]", "link[rel=\"stylesheet\"]"];
961    for sel_str in &selectors {
962        if let Ok(sel) = Selector::parse(sel_str) {
963            for el in doc.select(&sel) {
964                let attr = el
965                    .value()
966                    .attr("src")
967                    .or_else(|| el.value().attr("href"))
968                    .unwrap_or("");
969                if attr.contains("wp-content/plugins/") {
970                    if let Some(m) = plugin_re.as_ref().and_then(|r| r.captures(attr)) {
971                        plugins.insert(m.get(1).unwrap().as_str().to_string());
972                    }
973                }
974            }
975        }
976    }
977
978    // Known plugin signatures in HTML
979    for &(slug, _name) in WP_KNOWN_PLUGINS {
980        if html.contains(slug) {
981            plugins.insert(slug.to_string());
982        }
983    }
984
985    // Map slugs to names
986    plugins
987        .into_iter()
988        .map(|slug| {
989            WP_KNOWN_PLUGINS
990                .iter()
991                .find(|&&(s, _)| s == slug.as_str())
992                .map(|&(_, name)| name.to_string())
993                .unwrap_or_else(|| slug.replace('-', " "))
994        })
995        .collect()
996}
997
998async fn enumerate_wp_users(client: &Client, base_url: &str) -> Vec<WpUser> {
999    let url = format!("{}/wp-json/wp/v2/users", base_url);
1000    match client.get(&url).send().await {
1001        Ok(resp) if resp.status().is_success() => {
1002            if let Ok(users) = resp.json::<Vec<serde_json::Value>>().await {
1003                return users
1004                    .iter()
1005                    .filter_map(|u| {
1006                        Some(WpUser {
1007                            id: u.get("id")?.as_u64()?,
1008                            username: u.get("slug")?.as_str()?.to_string(),
1009                            display_name: u.get("name")?.as_str()?.to_string(),
1010                        })
1011                    })
1012                    .collect();
1013            }
1014        }
1015        _ => {}
1016    }
1017    vec![]
1018}
1019
1020async fn check_wp_endpoint(client: &Client, url: &str) -> bool {
1021    match client.get(url).send().await {
1022        Ok(r) => [200, 301, 302].contains(&r.status().as_u16()),
1023        Err(_) => false,
1024    }
1025}
1026
1027async fn check_wp_xmlrpc(client: &Client, base_url: &str) -> bool {
1028    let url = format!("{}/xmlrpc.php", base_url);
1029    match client.get(&url).send().await {
1030        Ok(r) if r.status().is_success() => r
1031            .text()
1032            .await
1033            .unwrap_or_default()
1034            .contains("XML-RPC server accepts POST requests only"),
1035        _ => false,
1036    }
1037}
1038
1039// ── 16. Security Score ──────────────────────────────────────────────────────
1040
1041fn calculate_score(
1042    headers: &HashMap<String, SecurityHeaderInfo>,
1043    vulns: &VulnerabilityInfo,
1044    disclosure: &DisclosureInfo,
1045    services: &SecurityServicesInfo,
1046    cookies: &CookieSecurityInfo,
1047    wp: &Option<WordPressAnalysis>,
1048) -> SecurityScoreResult {
1049    let mut score: i32 = 100;
1050    let mut issues = vec![];
1051    let mut recs = vec![];
1052
1053    // Security headers (−8 per missing)
1054    let missing = headers.values().filter(|h| !h.present).count() as i32;
1055    score -= missing * 8;
1056    if missing > 0 {
1057        issues.push(format!("{} critical security headers missing", missing));
1058        recs.push("Implement missing security headers".into());
1059    }
1060
1061    // Missing headers from vuln check (−5 each)
1062    score -= vulns.missing_security_headers.len() as i32 * 5;
1063
1064    // Insecure practices (−10 each)
1065    for p in &vulns.insecure_practices {
1066        score -= 10;
1067        issues.push(p.clone());
1068    }
1069
1070    // Info disclosure (−5 each)
1071    let disc_count = disclosure.server_info.len()
1072        + disclosure.technology_disclosure.len()
1073        + disclosure.file_exposure.len();
1074    score -= disc_count as i32 * 5;
1075
1076    // WAF bonus (+5)
1077    if !services.waf.is_empty() {
1078        score += 5;
1079        recs.push("WAF detected - Good security practice".into());
1080    }
1081
1082    // Cookie security
1083    if cookies.security_score < 70 && cookies.security_level != "N/A" {
1084        score -= 10;
1085        issues.push("Insecure cookie configuration".into());
1086        recs.push("Implement secure cookie flags".into());
1087    }
1088
1089    // WordPress
1090    if let Some(wp_info) = wp {
1091        for issue in &wp_info.security_issues {
1092            score -= 5;
1093            issues.push(issue.clone());
1094        }
1095    }
1096
1097    let final_score = score.clamp(0, 100) as u32;
1098
1099    let grade = match final_score {
1100        90..=100 => "A+",
1101        85..=89 => "A",
1102        80..=84 => "A-",
1103        75..=79 => "B+",
1104        70..=74 => "B",
1105        65..=69 => "B-",
1106        60..=64 => "C+",
1107        55..=59 => "C",
1108        50..=54 => "C-",
1109        40..=49 => "D",
1110        _ => "F",
1111    };
1112
1113    let risk = match final_score {
1114        80..=100 => "Low Risk",
1115        60..=79 => "Medium Risk",
1116        40..=59 => "High Risk",
1117        _ => "Critical Risk",
1118    };
1119
1120    SecurityScoreResult {
1121        overall_score: final_score,
1122        security_grade: grade.into(),
1123        risk_level: risk.into(),
1124        critical_issues: issues.into_iter().take(5).collect(),
1125        recommendations: recs.into_iter().take(5).collect(),
1126    }
1127}
1128
1129// ── Helpers ──────────────────────────────────────────────────────────────────
1130
1131fn get_header(headers: &reqwest::header::HeaderMap, name: &str) -> String {
1132    headers
1133        .get(name)
1134        .and_then(|v| v.to_str().ok())
1135        .unwrap_or("")
1136        .to_string()
1137}
1138
1139fn collect_script_srcs(doc: &Html) -> String {
1140    let sel = Selector::parse("script[src]").unwrap();
1141    doc.select(&sel)
1142        .filter_map(|el| el.value().attr("src"))
1143        .collect::<Vec<_>>()
1144        .join(" ")
1145        .to_lowercase()
1146}
1147
1148fn collect_stylesheet_hrefs(doc: &Html) -> String {
1149    let sel = Selector::parse("link[rel=\"stylesheet\"]").unwrap();
1150    doc.select(&sel)
1151        .filter_map(|el| el.value().attr("href"))
1152        .collect::<Vec<_>>()
1153        .join(" ")
1154        .to_lowercase()
1155}