1use regex::Regex;
2use reqwest::Client;
3use scraper::{Html, Selector};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::time::Duration;
7
8const 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#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct WebTechResult {
129 pub domain: String,
130 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 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 pub is_wordpress: bool,
148 pub wordpress_analysis: Option<WordPressAnalysis>,
149 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
220pub 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 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 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 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 }; report_progress(
372 &progress_tx,
373 75.0,
374 "Core technology fingerprinting complete",
375 "Success",
376 );
377
378 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 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
456fn 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
478fn 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
521fn 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
556fn 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
573fn 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
591fn 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
607fn 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
641fn 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
670fn 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
716fn 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
754fn 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
769fn 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
830fn 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 let confidence = if html.contains("wp-content/") && html.contains("wp-includes/") {
855 "High"
856 } else {
857 "Medium"
858 };
859
860 let users_found = enumerate_wp_users(client, &base_url).await;
862
863 let rest_api = check_wp_endpoint(client, &format!("{}/wp-json/", base_url)).await;
865
866 let xmlrpc = check_wp_xmlrpc(client, &base_url).await;
868
869 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 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 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 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 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 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 for &(slug, _name) in WP_KNOWN_PLUGINS {
980 if html.contains(slug) {
981 plugins.insert(slug.to_string());
982 }
983 }
984
985 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
1039fn 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 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 score -= vulns.missing_security_headers.len() as i32 * 5;
1063
1064 for p in &vulns.insecure_practices {
1066 score -= 10;
1067 issues.push(p.clone());
1068 }
1069
1070 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 if !services.waf.is_empty() {
1078 score += 5;
1079 recs.push("WAF detected - Good security practice".into());
1080 }
1081
1082 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 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
1129fn 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}