go_brrr/security/injection/
path_traversal.rs

1//! Path Traversal vulnerability detection for multiple programming languages.
2//!
3//! Detects potential path traversal (directory traversal) vulnerabilities by analyzing:
4//! - User input directly passed to file operation functions
5//! - Improper use of path joining functions (os.path.join is NOT a sanitizer!)
6//! - Missing path validation (realpath + startswith check)
7//! - Hardcoded "../" patterns in strings
8//!
9//! # Vulnerability Overview
10//!
11//! Path traversal allows attackers to access files outside the intended directory
12//! by manipulating path inputs with sequences like `../` or absolute paths.
13//!
14//! # Dangerous Patterns (Flagged)
15//!
16//! - `open(user_input)` without validation
17//! - `os.path.join(base, user_input)` - Still vulnerable to absolute paths and `..`!
18//! - `Path(user_input).read_text()` - Python pathlib
19//! - `fs.readFile(user_input)` - Node.js
20//! - `std::fs::read(user_input)` - Rust
21//! - `os.Open(path)` with user input - Go
22//! - `fopen(user_input)` - C
23//!
24//! # Safe Patterns (NOT Flagged)
25//!
26//! - `realpath()` followed by `startswith()` check
27//! - `os.path.basename()` to extract only filename
28//! - Allowlist validation against known filenames
29//! - Chroot or sandboxed file access
30//!
31//! # Symlink Attack Considerations
32//!
33//! Even with path validation, symlink attacks are possible:
34//! - Time-of-check to time-of-use (TOCTOU) race conditions
35//! - Symlinks created between validation and file access
36//! - Use `O_NOFOLLOW` flag or equivalent to prevent symlink following
37//!
38//! # Detection Strategy
39//!
40//! 1. Find file operation sinks (open, read, write, delete functions)
41//! 2. Track if path argument comes from user-controlled sources
42//! 3. Check for proper validation patterns nearby
43//! 4. Flag `../` patterns in static strings (hardcoded traversal bugs)
44//! 5. Flag `os.path.join` with user input (common misconception about safety)
45
46use std::collections::{HashMap, HashSet};
47use std::path::Path;
48
49use aho_corasick::AhoCorasick;
50use once_cell::sync::Lazy;
51use rayon::prelude::*;
52use serde::{Deserialize, Serialize};
53use streaming_iterator::StreamingIterator;
54use tree_sitter::{Node, Query, QueryCursor, Tree};
55
56use crate::callgraph::scanner::{ProjectScanner, ScanConfig};
57use crate::error::{Result, BrrrError};
58use crate::lang::LanguageRegistry;
59use crate::util::format_query_error;
60
61// =============================================================================
62// Static Pattern Matchers
63// =============================================================================
64
65/// Aho-Corasick automaton for detecting user input patterns.
66/// Single-pass multi-pattern matching replaces 18 sequential `.contains()` calls.
67static USER_INPUT_PATTERNS: Lazy<AhoCorasick> = Lazy::new(|| {
68    AhoCorasick::new([
69        "request", "req", "params", "query", "body", "input",
70        "user", "filename", "file_name", "filepath", "file_path",
71        "name", "path", "url", "uri", "data", "arg", "param",
72        "stdin", "argv",
73    ]).expect("USER_INPUT_PATTERNS: invalid patterns")
74});
75
76// =============================================================================
77// Type Definitions
78// =============================================================================
79
80/// Severity level for path traversal findings.
81#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
82#[serde(rename_all = "lowercase")]
83pub enum Severity {
84    /// Informational - pattern detected but likely not exploitable
85    Info,
86    /// Low severity - requires specific conditions to exploit
87    Low,
88    /// Medium severity - potential for file access outside intended directory
89    Medium,
90    /// High severity - likely exploitable path traversal
91    High,
92    /// Critical - easily exploitable with severe impact (arbitrary file read/write/delete)
93    Critical,
94}
95
96impl std::fmt::Display for Severity {
97    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98        match self {
99            Self::Info => write!(f, "INFO"),
100            Self::Low => write!(f, "LOW"),
101            Self::Medium => write!(f, "MEDIUM"),
102            Self::High => write!(f, "HIGH"),
103            Self::Critical => write!(f, "CRITICAL"),
104        }
105    }
106}
107
108/// Confidence level for the finding.
109#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
110#[serde(rename_all = "lowercase")]
111pub enum Confidence {
112    /// Low confidence - pattern match only, no data flow confirmation
113    Low,
114    /// Medium confidence - some indicators but incomplete validation check
115    Medium,
116    /// High confidence - clear user input to file operation without validation
117    High,
118}
119
120impl std::fmt::Display for Confidence {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        match self {
123            Self::Low => write!(f, "LOW"),
124            Self::Medium => write!(f, "MEDIUM"),
125            Self::High => write!(f, "HIGH"),
126        }
127    }
128}
129
130/// Type of file operation sink.
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
132#[serde(rename_all = "snake_case")]
133pub enum FileOperationType {
134    /// Reading file contents
135    Read,
136    /// Writing to files
137    Write,
138    /// Appending to files
139    Append,
140    /// Deleting/unlinking files
141    Delete,
142    /// File existence check
143    Exists,
144    /// Directory listing/traversal
145    ListDir,
146    /// File/directory creation
147    Create,
148    /// File move/rename
149    Move,
150    /// File copy
151    Copy,
152    /// Generic file open (mode unknown)
153    Open,
154}
155
156impl std::fmt::Display for FileOperationType {
157    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158        match self {
159            Self::Read => write!(f, "read"),
160            Self::Write => write!(f, "write"),
161            Self::Append => write!(f, "append"),
162            Self::Delete => write!(f, "delete"),
163            Self::Exists => write!(f, "exists"),
164            Self::ListDir => write!(f, "list_dir"),
165            Self::Create => write!(f, "create"),
166            Self::Move => write!(f, "move"),
167            Self::Copy => write!(f, "copy"),
168            Self::Open => write!(f, "open"),
169        }
170    }
171}
172
173/// Type of vulnerable pattern detected.
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
175#[serde(rename_all = "snake_case")]
176pub enum VulnerablePattern {
177    /// User input directly passed to file operation
178    DirectUserInput,
179    /// os.path.join with user input (NOT a sanitizer!)
180    UnsafePathJoin,
181    /// Path concatenation with user input
182    PathConcatenation,
183    /// Hardcoded "../" pattern in string literal
184    HardcodedTraversal,
185    /// Variable passed without visible validation
186    UnvalidatedVariable,
187    /// Template/f-string with path interpolation
188    PathInterpolation,
189    /// Missing realpath + startswith validation
190    MissingValidation,
191}
192
193impl std::fmt::Display for VulnerablePattern {
194    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195        match self {
196            Self::DirectUserInput => write!(f, "direct_user_input"),
197            Self::UnsafePathJoin => write!(f, "unsafe_path_join"),
198            Self::PathConcatenation => write!(f, "path_concatenation"),
199            Self::HardcodedTraversal => write!(f, "hardcoded_traversal"),
200            Self::UnvalidatedVariable => write!(f, "unvalidated_variable"),
201            Self::PathInterpolation => write!(f, "path_interpolation"),
202            Self::MissingValidation => write!(f, "missing_validation"),
203        }
204    }
205}
206
207/// Source location in code.
208#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
209pub struct SourceLocation {
210    /// File path
211    pub file: String,
212    /// Line number (1-indexed)
213    pub line: usize,
214    /// Column number (1-indexed)
215    pub column: usize,
216    /// End line number (1-indexed)
217    pub end_line: usize,
218    /// End column number (1-indexed)
219    pub end_column: usize,
220}
221
222impl std::fmt::Display for SourceLocation {
223    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224        write!(f, "{}:{}:{}", self.file, self.line, self.column)
225    }
226}
227
228/// A path traversal finding.
229#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct PathTraversalFinding {
231    /// Location of the vulnerable sink call
232    pub location: SourceLocation,
233    /// Severity of the vulnerability
234    pub severity: Severity,
235    /// Name of the file operation function being called
236    pub sink_function: String,
237    /// Type of file operation
238    pub operation_type: FileOperationType,
239    /// The path expression reaching the sink
240    pub path_expression: String,
241    /// Confidence level of the finding
242    pub confidence: Confidence,
243    /// Type of vulnerable pattern detected
244    pub pattern: VulnerablePattern,
245    /// Variables involved in the path construction
246    #[serde(skip_serializing_if = "Vec::is_empty")]
247    pub involved_variables: Vec<String>,
248    /// Code snippet showing the vulnerable pattern
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub code_snippet: Option<String>,
251    /// Human-readable description
252    pub description: String,
253    /// Remediation advice
254    pub remediation: String,
255    /// Whether symlink attacks are also possible
256    pub symlink_risk: bool,
257}
258
259/// Result of scanning for path traversal vulnerabilities.
260#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct ScanResult {
262    /// All findings
263    pub findings: Vec<PathTraversalFinding>,
264    /// Number of files scanned
265    pub files_scanned: usize,
266    /// Number of file operation sinks found
267    pub sinks_found: usize,
268    /// Counts by severity
269    pub severity_counts: HashMap<String, usize>,
270    /// Language detected
271    pub language: String,
272}
273
274// =============================================================================
275// File Operation Sink Definitions
276// =============================================================================
277
278/// Definition of a file operation sink.
279#[derive(Debug, Clone)]
280pub struct FileSink {
281    /// Language this sink applies to
282    pub language: &'static str,
283    /// Module or namespace (e.g., "os", "fs", "std::fs")
284    pub module: Option<&'static str>,
285    /// Function name (e.g., "open", "read", "readFile")
286    pub function: &'static str,
287    /// Argument index that receives the path (0-indexed)
288    pub path_arg_index: usize,
289    /// Type of file operation
290    pub operation_type: FileOperationType,
291    /// Base severity when this sink is exploited
292    pub severity: Severity,
293    /// Description of the sink
294    pub description: &'static str,
295}
296
297/// Get all known file operation sinks for a language.
298pub fn get_file_sinks(language: &str) -> Vec<FileSink> {
299    match language {
300        "python" => python_sinks(),
301        "typescript" | "javascript" => typescript_sinks(),
302        "rust" => rust_sinks(),
303        "go" => go_sinks(),
304        "c" | "cpp" => c_sinks(),
305        _ => vec![],
306    }
307}
308
309fn python_sinks() -> Vec<FileSink> {
310    vec![
311        // Built-in open
312        FileSink {
313            language: "python",
314            module: None,
315            function: "open",
316            path_arg_index: 0,
317            operation_type: FileOperationType::Open,
318            severity: Severity::High,
319            description: "Built-in open() function",
320        },
321        // pathlib methods
322        FileSink {
323            language: "python",
324            module: Some("pathlib"),
325            function: "read_text",
326            path_arg_index: 0,
327            operation_type: FileOperationType::Read,
328            severity: Severity::High,
329            description: "Path.read_text() reads file contents",
330        },
331        FileSink {
332            language: "python",
333            module: Some("pathlib"),
334            function: "read_bytes",
335            path_arg_index: 0,
336            operation_type: FileOperationType::Read,
337            severity: Severity::High,
338            description: "Path.read_bytes() reads binary file contents",
339        },
340        FileSink {
341            language: "python",
342            module: Some("pathlib"),
343            function: "write_text",
344            path_arg_index: 0,
345            operation_type: FileOperationType::Write,
346            severity: Severity::Critical,
347            description: "Path.write_text() writes file contents",
348        },
349        FileSink {
350            language: "python",
351            module: Some("pathlib"),
352            function: "write_bytes",
353            path_arg_index: 0,
354            operation_type: FileOperationType::Write,
355            severity: Severity::Critical,
356            description: "Path.write_bytes() writes binary contents",
357        },
358        FileSink {
359            language: "python",
360            module: Some("pathlib"),
361            function: "unlink",
362            path_arg_index: 0,
363            operation_type: FileOperationType::Delete,
364            severity: Severity::Critical,
365            description: "Path.unlink() deletes file",
366        },
367        FileSink {
368            language: "python",
369            module: Some("pathlib"),
370            function: "rmdir",
371            path_arg_index: 0,
372            operation_type: FileOperationType::Delete,
373            severity: Severity::Critical,
374            description: "Path.rmdir() removes directory",
375        },
376        FileSink {
377            language: "python",
378            module: Some("pathlib"),
379            function: "mkdir",
380            path_arg_index: 0,
381            operation_type: FileOperationType::Create,
382            severity: Severity::Medium,
383            description: "Path.mkdir() creates directory",
384        },
385        // os module
386        FileSink {
387            language: "python",
388            module: Some("os"),
389            function: "remove",
390            path_arg_index: 0,
391            operation_type: FileOperationType::Delete,
392            severity: Severity::Critical,
393            description: "os.remove() deletes file",
394        },
395        FileSink {
396            language: "python",
397            module: Some("os"),
398            function: "unlink",
399            path_arg_index: 0,
400            operation_type: FileOperationType::Delete,
401            severity: Severity::Critical,
402            description: "os.unlink() deletes file",
403        },
404        FileSink {
405            language: "python",
406            module: Some("os"),
407            function: "rmdir",
408            path_arg_index: 0,
409            operation_type: FileOperationType::Delete,
410            severity: Severity::Critical,
411            description: "os.rmdir() removes directory",
412        },
413        FileSink {
414            language: "python",
415            module: Some("os"),
416            function: "mkdir",
417            path_arg_index: 0,
418            operation_type: FileOperationType::Create,
419            severity: Severity::Medium,
420            description: "os.mkdir() creates directory",
421        },
422        FileSink {
423            language: "python",
424            module: Some("os"),
425            function: "makedirs",
426            path_arg_index: 0,
427            operation_type: FileOperationType::Create,
428            severity: Severity::Medium,
429            description: "os.makedirs() creates directory tree",
430        },
431        FileSink {
432            language: "python",
433            module: Some("os"),
434            function: "listdir",
435            path_arg_index: 0,
436            operation_type: FileOperationType::ListDir,
437            severity: Severity::Medium,
438            description: "os.listdir() lists directory contents",
439        },
440        FileSink {
441            language: "python",
442            module: Some("os"),
443            function: "rename",
444            path_arg_index: 0,
445            operation_type: FileOperationType::Move,
446            severity: Severity::Critical,
447            description: "os.rename() moves/renames file",
448        },
449        FileSink {
450            language: "python",
451            module: Some("os"),
452            function: "replace",
453            path_arg_index: 0,
454            operation_type: FileOperationType::Move,
455            severity: Severity::Critical,
456            description: "os.replace() replaces file atomically",
457        },
458        // os.path - join is NOT a sink itself but flagged specially
459        FileSink {
460            language: "python",
461            module: Some("os.path"),
462            function: "join",
463            path_arg_index: 1, // Second arg is typically user input
464            operation_type: FileOperationType::Open,
465            severity: Severity::High, // Misconception that join sanitizes
466            description: "os.path.join() does NOT sanitize - still vulnerable to absolute paths and ..",
467        },
468        // shutil module
469        FileSink {
470            language: "python",
471            module: Some("shutil"),
472            function: "copy",
473            path_arg_index: 0,
474            operation_type: FileOperationType::Copy,
475            severity: Severity::High,
476            description: "shutil.copy() copies file",
477        },
478        FileSink {
479            language: "python",
480            module: Some("shutil"),
481            function: "copy2",
482            path_arg_index: 0,
483            operation_type: FileOperationType::Copy,
484            severity: Severity::High,
485            description: "shutil.copy2() copies file with metadata",
486        },
487        FileSink {
488            language: "python",
489            module: Some("shutil"),
490            function: "copyfile",
491            path_arg_index: 0,
492            operation_type: FileOperationType::Copy,
493            severity: Severity::High,
494            description: "shutil.copyfile() copies file contents",
495        },
496        FileSink {
497            language: "python",
498            module: Some("shutil"),
499            function: "copytree",
500            path_arg_index: 0,
501            operation_type: FileOperationType::Copy,
502            severity: Severity::High,
503            description: "shutil.copytree() copies entire directory",
504        },
505        FileSink {
506            language: "python",
507            module: Some("shutil"),
508            function: "rmtree",
509            path_arg_index: 0,
510            operation_type: FileOperationType::Delete,
511            severity: Severity::Critical,
512            description: "shutil.rmtree() deletes entire directory tree",
513        },
514        FileSink {
515            language: "python",
516            module: Some("shutil"),
517            function: "move",
518            path_arg_index: 0,
519            operation_type: FileOperationType::Move,
520            severity: Severity::Critical,
521            description: "shutil.move() moves file or directory",
522        },
523    ]
524}
525
526fn typescript_sinks() -> Vec<FileSink> {
527    vec![
528        // fs module - callback style
529        FileSink {
530            language: "typescript",
531            module: Some("fs"),
532            function: "readFile",
533            path_arg_index: 0,
534            operation_type: FileOperationType::Read,
535            severity: Severity::High,
536            description: "fs.readFile() reads file contents",
537        },
538        FileSink {
539            language: "typescript",
540            module: Some("fs"),
541            function: "readFileSync",
542            path_arg_index: 0,
543            operation_type: FileOperationType::Read,
544            severity: Severity::High,
545            description: "fs.readFileSync() synchronously reads file",
546        },
547        FileSink {
548            language: "typescript",
549            module: Some("fs"),
550            function: "writeFile",
551            path_arg_index: 0,
552            operation_type: FileOperationType::Write,
553            severity: Severity::Critical,
554            description: "fs.writeFile() writes file contents",
555        },
556        FileSink {
557            language: "typescript",
558            module: Some("fs"),
559            function: "writeFileSync",
560            path_arg_index: 0,
561            operation_type: FileOperationType::Write,
562            severity: Severity::Critical,
563            description: "fs.writeFileSync() synchronously writes file",
564        },
565        FileSink {
566            language: "typescript",
567            module: Some("fs"),
568            function: "appendFile",
569            path_arg_index: 0,
570            operation_type: FileOperationType::Append,
571            severity: Severity::High,
572            description: "fs.appendFile() appends to file",
573        },
574        FileSink {
575            language: "typescript",
576            module: Some("fs"),
577            function: "appendFileSync",
578            path_arg_index: 0,
579            operation_type: FileOperationType::Append,
580            severity: Severity::High,
581            description: "fs.appendFileSync() synchronously appends",
582        },
583        FileSink {
584            language: "typescript",
585            module: Some("fs"),
586            function: "unlink",
587            path_arg_index: 0,
588            operation_type: FileOperationType::Delete,
589            severity: Severity::Critical,
590            description: "fs.unlink() deletes file",
591        },
592        FileSink {
593            language: "typescript",
594            module: Some("fs"),
595            function: "unlinkSync",
596            path_arg_index: 0,
597            operation_type: FileOperationType::Delete,
598            severity: Severity::Critical,
599            description: "fs.unlinkSync() synchronously deletes file",
600        },
601        FileSink {
602            language: "typescript",
603            module: Some("fs"),
604            function: "rmdir",
605            path_arg_index: 0,
606            operation_type: FileOperationType::Delete,
607            severity: Severity::Critical,
608            description: "fs.rmdir() removes directory",
609        },
610        FileSink {
611            language: "typescript",
612            module: Some("fs"),
613            function: "rm",
614            path_arg_index: 0,
615            operation_type: FileOperationType::Delete,
616            severity: Severity::Critical,
617            description: "fs.rm() removes file or directory",
618        },
619        FileSink {
620            language: "typescript",
621            module: Some("fs"),
622            function: "mkdir",
623            path_arg_index: 0,
624            operation_type: FileOperationType::Create,
625            severity: Severity::Medium,
626            description: "fs.mkdir() creates directory",
627        },
628        FileSink {
629            language: "typescript",
630            module: Some("fs"),
631            function: "readdir",
632            path_arg_index: 0,
633            operation_type: FileOperationType::ListDir,
634            severity: Severity::Medium,
635            description: "fs.readdir() lists directory contents",
636        },
637        FileSink {
638            language: "typescript",
639            module: Some("fs"),
640            function: "rename",
641            path_arg_index: 0,
642            operation_type: FileOperationType::Move,
643            severity: Severity::Critical,
644            description: "fs.rename() moves/renames file",
645        },
646        FileSink {
647            language: "typescript",
648            module: Some("fs"),
649            function: "copyFile",
650            path_arg_index: 0,
651            operation_type: FileOperationType::Copy,
652            severity: Severity::High,
653            description: "fs.copyFile() copies file",
654        },
655        FileSink {
656            language: "typescript",
657            module: Some("fs"),
658            function: "createReadStream",
659            path_arg_index: 0,
660            operation_type: FileOperationType::Read,
661            severity: Severity::High,
662            description: "fs.createReadStream() opens read stream",
663        },
664        FileSink {
665            language: "typescript",
666            module: Some("fs"),
667            function: "createWriteStream",
668            path_arg_index: 0,
669            operation_type: FileOperationType::Write,
670            severity: Severity::Critical,
671            description: "fs.createWriteStream() opens write stream",
672        },
673        // fs/promises module
674        FileSink {
675            language: "typescript",
676            module: Some("fs/promises"),
677            function: "readFile",
678            path_arg_index: 0,
679            operation_type: FileOperationType::Read,
680            severity: Severity::High,
681            description: "fsPromises.readFile() async reads file",
682        },
683        FileSink {
684            language: "typescript",
685            module: Some("fs/promises"),
686            function: "writeFile",
687            path_arg_index: 0,
688            operation_type: FileOperationType::Write,
689            severity: Severity::Critical,
690            description: "fsPromises.writeFile() async writes file",
691        },
692        // path.join - NOT a sanitizer!
693        FileSink {
694            language: "typescript",
695            module: Some("path"),
696            function: "join",
697            path_arg_index: 1,
698            operation_type: FileOperationType::Open,
699            severity: Severity::High,
700            description: "path.join() does NOT sanitize - still vulnerable to ..",
701        },
702        FileSink {
703            language: "typescript",
704            module: Some("path"),
705            function: "resolve",
706            path_arg_index: 0,
707            operation_type: FileOperationType::Open,
708            severity: Severity::High,
709            description: "path.resolve() resolves to absolute path but doesn't validate",
710        },
711    ]
712}
713
714fn rust_sinks() -> Vec<FileSink> {
715    vec![
716        // std::fs module
717        FileSink {
718            language: "rust",
719            module: Some("std::fs"),
720            function: "read",
721            path_arg_index: 0,
722            operation_type: FileOperationType::Read,
723            severity: Severity::High,
724            description: "std::fs::read() reads file to Vec<u8>",
725        },
726        FileSink {
727            language: "rust",
728            module: Some("std::fs"),
729            function: "read_to_string",
730            path_arg_index: 0,
731            operation_type: FileOperationType::Read,
732            severity: Severity::High,
733            description: "std::fs::read_to_string() reads file to String",
734        },
735        FileSink {
736            language: "rust",
737            module: Some("std::fs"),
738            function: "write",
739            path_arg_index: 0,
740            operation_type: FileOperationType::Write,
741            severity: Severity::Critical,
742            description: "std::fs::write() writes data to file",
743        },
744        FileSink {
745            language: "rust",
746            module: Some("std::fs"),
747            function: "remove_file",
748            path_arg_index: 0,
749            operation_type: FileOperationType::Delete,
750            severity: Severity::Critical,
751            description: "std::fs::remove_file() deletes file",
752        },
753        FileSink {
754            language: "rust",
755            module: Some("std::fs"),
756            function: "remove_dir",
757            path_arg_index: 0,
758            operation_type: FileOperationType::Delete,
759            severity: Severity::Critical,
760            description: "std::fs::remove_dir() removes empty directory",
761        },
762        FileSink {
763            language: "rust",
764            module: Some("std::fs"),
765            function: "remove_dir_all",
766            path_arg_index: 0,
767            operation_type: FileOperationType::Delete,
768            severity: Severity::Critical,
769            description: "std::fs::remove_dir_all() recursively deletes directory",
770        },
771        FileSink {
772            language: "rust",
773            module: Some("std::fs"),
774            function: "create_dir",
775            path_arg_index: 0,
776            operation_type: FileOperationType::Create,
777            severity: Severity::Medium,
778            description: "std::fs::create_dir() creates directory",
779        },
780        FileSink {
781            language: "rust",
782            module: Some("std::fs"),
783            function: "create_dir_all",
784            path_arg_index: 0,
785            operation_type: FileOperationType::Create,
786            severity: Severity::Medium,
787            description: "std::fs::create_dir_all() creates directory tree",
788        },
789        FileSink {
790            language: "rust",
791            module: Some("std::fs"),
792            function: "copy",
793            path_arg_index: 0,
794            operation_type: FileOperationType::Copy,
795            severity: Severity::High,
796            description: "std::fs::copy() copies file contents",
797        },
798        FileSink {
799            language: "rust",
800            module: Some("std::fs"),
801            function: "rename",
802            path_arg_index: 0,
803            operation_type: FileOperationType::Move,
804            severity: Severity::Critical,
805            description: "std::fs::rename() moves/renames file",
806        },
807        FileSink {
808            language: "rust",
809            module: Some("std::fs"),
810            function: "read_dir",
811            path_arg_index: 0,
812            operation_type: FileOperationType::ListDir,
813            severity: Severity::Medium,
814            description: "std::fs::read_dir() lists directory contents",
815        },
816        // File::open
817        FileSink {
818            language: "rust",
819            module: Some("std::fs"),
820            function: "File::open",
821            path_arg_index: 0,
822            operation_type: FileOperationType::Read,
823            severity: Severity::High,
824            description: "File::open() opens file for reading",
825        },
826        FileSink {
827            language: "rust",
828            module: Some("std::fs"),
829            function: "File::create",
830            path_arg_index: 0,
831            operation_type: FileOperationType::Write,
832            severity: Severity::Critical,
833            description: "File::create() creates/truncates file",
834        },
835        // Path::new with user input
836        FileSink {
837            language: "rust",
838            module: Some("std::path"),
839            function: "Path::new",
840            path_arg_index: 0,
841            operation_type: FileOperationType::Open,
842            severity: Severity::Medium,
843            description: "Path::new() with user input may enable traversal",
844        },
845        // tokio::fs for async operations
846        FileSink {
847            language: "rust",
848            module: Some("tokio::fs"),
849            function: "read",
850            path_arg_index: 0,
851            operation_type: FileOperationType::Read,
852            severity: Severity::High,
853            description: "tokio::fs::read() async reads file",
854        },
855        FileSink {
856            language: "rust",
857            module: Some("tokio::fs"),
858            function: "write",
859            path_arg_index: 0,
860            operation_type: FileOperationType::Write,
861            severity: Severity::Critical,
862            description: "tokio::fs::write() async writes file",
863        },
864    ]
865}
866
867fn go_sinks() -> Vec<FileSink> {
868    vec![
869        // os package
870        FileSink {
871            language: "go",
872            module: Some("os"),
873            function: "Open",
874            path_arg_index: 0,
875            operation_type: FileOperationType::Read,
876            severity: Severity::High,
877            description: "os.Open() opens file for reading",
878        },
879        FileSink {
880            language: "go",
881            module: Some("os"),
882            function: "OpenFile",
883            path_arg_index: 0,
884            operation_type: FileOperationType::Open,
885            severity: Severity::High,
886            description: "os.OpenFile() opens file with specified flags",
887        },
888        FileSink {
889            language: "go",
890            module: Some("os"),
891            function: "Create",
892            path_arg_index: 0,
893            operation_type: FileOperationType::Write,
894            severity: Severity::Critical,
895            description: "os.Create() creates/truncates file",
896        },
897        FileSink {
898            language: "go",
899            module: Some("os"),
900            function: "Remove",
901            path_arg_index: 0,
902            operation_type: FileOperationType::Delete,
903            severity: Severity::Critical,
904            description: "os.Remove() deletes file",
905        },
906        FileSink {
907            language: "go",
908            module: Some("os"),
909            function: "RemoveAll",
910            path_arg_index: 0,
911            operation_type: FileOperationType::Delete,
912            severity: Severity::Critical,
913            description: "os.RemoveAll() recursively deletes path",
914        },
915        FileSink {
916            language: "go",
917            module: Some("os"),
918            function: "Rename",
919            path_arg_index: 0,
920            operation_type: FileOperationType::Move,
921            severity: Severity::Critical,
922            description: "os.Rename() moves/renames file",
923        },
924        FileSink {
925            language: "go",
926            module: Some("os"),
927            function: "Mkdir",
928            path_arg_index: 0,
929            operation_type: FileOperationType::Create,
930            severity: Severity::Medium,
931            description: "os.Mkdir() creates directory",
932        },
933        FileSink {
934            language: "go",
935            module: Some("os"),
936            function: "MkdirAll",
937            path_arg_index: 0,
938            operation_type: FileOperationType::Create,
939            severity: Severity::Medium,
940            description: "os.MkdirAll() creates directory tree",
941        },
942        FileSink {
943            language: "go",
944            module: Some("os"),
945            function: "ReadDir",
946            path_arg_index: 0,
947            operation_type: FileOperationType::ListDir,
948            severity: Severity::Medium,
949            description: "os.ReadDir() lists directory entries",
950        },
951        FileSink {
952            language: "go",
953            module: Some("os"),
954            function: "ReadFile",
955            path_arg_index: 0,
956            operation_type: FileOperationType::Read,
957            severity: Severity::High,
958            description: "os.ReadFile() reads entire file",
959        },
960        FileSink {
961            language: "go",
962            module: Some("os"),
963            function: "WriteFile",
964            path_arg_index: 0,
965            operation_type: FileOperationType::Write,
966            severity: Severity::Critical,
967            description: "os.WriteFile() writes entire file",
968        },
969        // ioutil (deprecated but still used)
970        FileSink {
971            language: "go",
972            module: Some("ioutil"),
973            function: "ReadFile",
974            path_arg_index: 0,
975            operation_type: FileOperationType::Read,
976            severity: Severity::High,
977            description: "ioutil.ReadFile() reads entire file",
978        },
979        FileSink {
980            language: "go",
981            module: Some("ioutil"),
982            function: "WriteFile",
983            path_arg_index: 0,
984            operation_type: FileOperationType::Write,
985            severity: Severity::Critical,
986            description: "ioutil.WriteFile() writes entire file",
987        },
988        FileSink {
989            language: "go",
990            module: Some("ioutil"),
991            function: "ReadDir",
992            path_arg_index: 0,
993            operation_type: FileOperationType::ListDir,
994            severity: Severity::Medium,
995            description: "ioutil.ReadDir() lists directory",
996        },
997        // filepath.Join - NOT a sanitizer!
998        FileSink {
999            language: "go",
1000            module: Some("filepath"),
1001            function: "Join",
1002            path_arg_index: 1,
1003            operation_type: FileOperationType::Open,
1004            severity: Severity::High,
1005            description: "filepath.Join() does NOT sanitize - cleans but allows ..",
1006        },
1007    ]
1008}
1009
1010fn c_sinks() -> Vec<FileSink> {
1011    vec![
1012        // Standard C library
1013        FileSink {
1014            language: "c",
1015            module: None,
1016            function: "fopen",
1017            path_arg_index: 0,
1018            operation_type: FileOperationType::Open,
1019            severity: Severity::High,
1020            description: "fopen() opens file stream",
1021        },
1022        FileSink {
1023            language: "c",
1024            module: None,
1025            function: "freopen",
1026            path_arg_index: 0,
1027            operation_type: FileOperationType::Open,
1028            severity: Severity::High,
1029            description: "freopen() reopens file stream",
1030        },
1031        FileSink {
1032            language: "c",
1033            module: None,
1034            function: "open",
1035            path_arg_index: 0,
1036            operation_type: FileOperationType::Open,
1037            severity: Severity::High,
1038            description: "open() POSIX file open",
1039        },
1040        FileSink {
1041            language: "c",
1042            module: None,
1043            function: "openat",
1044            path_arg_index: 1,
1045            operation_type: FileOperationType::Open,
1046            severity: Severity::High,
1047            description: "openat() opens file relative to directory fd",
1048        },
1049        FileSink {
1050            language: "c",
1051            module: None,
1052            function: "creat",
1053            path_arg_index: 0,
1054            operation_type: FileOperationType::Write,
1055            severity: Severity::Critical,
1056            description: "creat() creates file",
1057        },
1058        FileSink {
1059            language: "c",
1060            module: None,
1061            function: "remove",
1062            path_arg_index: 0,
1063            operation_type: FileOperationType::Delete,
1064            severity: Severity::Critical,
1065            description: "remove() deletes file",
1066        },
1067        FileSink {
1068            language: "c",
1069            module: None,
1070            function: "unlink",
1071            path_arg_index: 0,
1072            operation_type: FileOperationType::Delete,
1073            severity: Severity::Critical,
1074            description: "unlink() removes file",
1075        },
1076        FileSink {
1077            language: "c",
1078            module: None,
1079            function: "rmdir",
1080            path_arg_index: 0,
1081            operation_type: FileOperationType::Delete,
1082            severity: Severity::Critical,
1083            description: "rmdir() removes directory",
1084        },
1085        FileSink {
1086            language: "c",
1087            module: None,
1088            function: "mkdir",
1089            path_arg_index: 0,
1090            operation_type: FileOperationType::Create,
1091            severity: Severity::Medium,
1092            description: "mkdir() creates directory",
1093        },
1094        FileSink {
1095            language: "c",
1096            module: None,
1097            function: "rename",
1098            path_arg_index: 0,
1099            operation_type: FileOperationType::Move,
1100            severity: Severity::Critical,
1101            description: "rename() moves/renames file",
1102        },
1103        FileSink {
1104            language: "c",
1105            module: None,
1106            function: "opendir",
1107            path_arg_index: 0,
1108            operation_type: FileOperationType::ListDir,
1109            severity: Severity::Medium,
1110            description: "opendir() opens directory for reading",
1111        },
1112        FileSink {
1113            language: "c",
1114            module: None,
1115            function: "stat",
1116            path_arg_index: 0,
1117            operation_type: FileOperationType::Exists,
1118            severity: Severity::Low,
1119            description: "stat() gets file status",
1120        },
1121        FileSink {
1122            language: "c",
1123            module: None,
1124            function: "lstat",
1125            path_arg_index: 0,
1126            operation_type: FileOperationType::Exists,
1127            severity: Severity::Low,
1128            description: "lstat() gets symlink status",
1129        },
1130        FileSink {
1131            language: "c",
1132            module: None,
1133            function: "access",
1134            path_arg_index: 0,
1135            operation_type: FileOperationType::Exists,
1136            severity: Severity::Low,
1137            description: "access() checks file permissions",
1138        },
1139    ]
1140}
1141
1142// =============================================================================
1143// Tree-sitter Queries
1144// =============================================================================
1145
1146/// Python sink detection query.
1147const PYTHON_SINK_QUERY: &str = r#"
1148; Built-in open()
1149(call function: (identifier) @func
1150  (#eq? @func "open")
1151  arguments: (argument_list) @args) @sink
1152
1153; pathlib Path methods - read/write
1154(call function: (attribute object: (call function: (identifier) @pathlib) attribute: (identifier) @method)
1155  (#eq? @pathlib "Path")
1156  (#any-of? @method "read_text" "read_bytes" "write_text" "write_bytes" "unlink" "rmdir" "mkdir")
1157  arguments: (argument_list) @args) @sink
1158
1159; pathlib Path(x).method() pattern
1160(call function: (attribute attribute: (identifier) @method)
1161  (#any-of? @method "read_text" "read_bytes" "write_text" "write_bytes" "unlink" "rmdir" "mkdir" "open")) @sink
1162
1163; os module functions
1164(call function: (attribute object: (identifier) @module attribute: (identifier) @func)
1165  (#eq? @module "os")
1166  (#any-of? @func "remove" "unlink" "rmdir" "mkdir" "makedirs" "listdir" "rename" "replace")
1167  arguments: (argument_list) @args) @sink
1168
1169; os.path.join - IMPORTANT: flag this as vulnerable pattern
1170(call function: (attribute object: (attribute object: (identifier) @os attribute: (identifier) @path) attribute: (identifier) @func)
1171  (#eq? @os "os")
1172  (#eq? @path "path")
1173  (#eq? @func "join")
1174  arguments: (argument_list) @args) @join_sink
1175
1176; shutil functions
1177(call function: (attribute object: (identifier) @module attribute: (identifier) @func)
1178  (#eq? @module "shutil")
1179  (#any-of? @func "copy" "copy2" "copyfile" "copytree" "rmtree" "move")
1180  arguments: (argument_list) @args) @sink
1181
1182; String literals containing ".." (hardcoded traversal)
1183(string) @string_lit
1184"#;
1185
1186/// TypeScript/JavaScript sink detection query.
1187const TYPESCRIPT_SINK_QUERY: &str = r#"
1188; fs module functions
1189(call_expression function: (member_expression object: (identifier) @module property: (property_identifier) @func)
1190  (#any-of? @module "fs" "fsp")
1191  (#any-of? @func "readFile" "readFileSync" "writeFile" "writeFileSync" "appendFile" "appendFileSync"
1192             "unlink" "unlinkSync" "rmdir" "rm" "mkdir" "readdir" "rename" "copyFile"
1193             "createReadStream" "createWriteStream")
1194  arguments: (arguments) @args) @sink
1195
1196; require('fs').method pattern
1197(call_expression function: (member_expression object: (call_expression function: (identifier) @req arguments: (arguments (string) @mod))
1198    property: (property_identifier) @func)
1199  (#eq? @req "require")
1200  (#match? @mod "fs")
1201  (#any-of? @func "readFile" "readFileSync" "writeFile" "writeFileSync" "unlink" "unlinkSync")
1202  arguments: (arguments) @args) @sink
1203
1204; path.join - flag as vulnerable
1205(call_expression function: (member_expression object: (identifier) @module property: (property_identifier) @func)
1206  (#eq? @module "path")
1207  (#any-of? @func "join" "resolve")
1208  arguments: (arguments) @args) @join_sink
1209
1210; String literals containing ".."
1211(string) @string_lit
1212(template_string) @template_lit
1213"#;
1214
1215/// Go sink detection query.
1216const GO_SINK_QUERY: &str = r#"
1217; os package functions
1218(call_expression function: (selector_expression operand: (identifier) @pkg field: (field_identifier) @func)
1219  (#eq? @pkg "os")
1220  (#any-of? @func "Open" "OpenFile" "Create" "Remove" "RemoveAll" "Rename" "Mkdir" "MkdirAll"
1221            "ReadDir" "ReadFile" "WriteFile")
1222  arguments: (argument_list) @args) @sink
1223
1224; ioutil functions (deprecated but used)
1225(call_expression function: (selector_expression operand: (identifier) @pkg field: (field_identifier) @func)
1226  (#eq? @pkg "ioutil")
1227  (#any-of? @func "ReadFile" "WriteFile" "ReadDir")
1228  arguments: (argument_list) @args) @sink
1229
1230; filepath.Join - flag as vulnerable pattern
1231(call_expression function: (selector_expression operand: (identifier) @pkg field: (field_identifier) @func)
1232  (#eq? @pkg "filepath")
1233  (#eq? @func "Join")
1234  arguments: (argument_list) @args) @join_sink
1235
1236; String literals containing ".."
1237(interpreted_string_literal) @string_lit
1238(raw_string_literal) @string_lit
1239"#;
1240
1241/// Rust sink detection query.
1242const RUST_SINK_QUERY: &str = r#"
1243; std::fs functions
1244(call_expression function: (scoped_identifier) @func
1245  (#match? @func "fs::(read|read_to_string|write|remove_file|remove_dir|remove_dir_all|create_dir|create_dir_all|copy|rename|read_dir)")
1246  arguments: (arguments) @args) @sink
1247
1248; File::open and File::create
1249(call_expression function: (scoped_identifier) @func
1250  (#match? @func "File::(open|create)")
1251  arguments: (arguments) @args) @sink
1252
1253; Path::new with user input
1254(call_expression function: (scoped_identifier) @func
1255  (#match? @func "Path::new")
1256  arguments: (arguments) @args) @path_new_sink
1257
1258; tokio::fs functions
1259(call_expression function: (scoped_identifier) @func
1260  (#match? @func "tokio::fs::")
1261  arguments: (arguments) @args) @sink
1262
1263; String literals containing ".."
1264(string_literal) @string_lit
1265"#;
1266
1267/// C sink detection query.
1268const C_SINK_QUERY: &str = r#"
1269; File operations
1270(call_expression function: (identifier) @func
1271  (#any-of? @func "fopen" "freopen" "open" "creat" "remove" "unlink" "rmdir" "mkdir" "rename" "opendir" "stat" "lstat" "access")
1272  arguments: (argument_list) @args) @sink
1273
1274; openat has path at index 1
1275(call_expression function: (identifier) @func
1276  (#eq? @func "openat")
1277  arguments: (argument_list) @args) @sink_openat
1278
1279; String literals containing ".."
1280(string_literal) @string_lit
1281"#;
1282
1283/// Get tree-sitter query for detecting file sinks in a language.
1284fn get_sink_query(language: &str) -> Option<&'static str> {
1285    match language {
1286        "python" => Some(PYTHON_SINK_QUERY),
1287        "typescript" | "javascript" => Some(TYPESCRIPT_SINK_QUERY),
1288        "go" => Some(GO_SINK_QUERY),
1289        "rust" => Some(RUST_SINK_QUERY),
1290        "c" | "cpp" => Some(C_SINK_QUERY),
1291        _ => None,
1292    }
1293}
1294
1295// =============================================================================
1296// Scanning Implementation
1297// =============================================================================
1298
1299/// Scan a directory for path traversal vulnerabilities.
1300///
1301/// # Arguments
1302///
1303/// * `path` - Directory to scan
1304/// * `language` - Optional language filter (scans all supported languages if None)
1305///
1306/// # Returns
1307///
1308/// Vector of path traversal findings.
1309pub fn scan_path_traversal(path: &Path, language: Option<&str>) -> Result<Vec<PathTraversalFinding>> {
1310    let path_str = path.to_str().ok_or_else(|| {
1311        BrrrError::InvalidArgument("Invalid path encoding".to_string())
1312    })?;
1313
1314    let scanner = ProjectScanner::new(path_str)?;
1315    let config = match language {
1316        Some(lang) => ScanConfig::for_language(lang),
1317        None => ScanConfig::default(),
1318    };
1319
1320    let scan_result = scanner.scan_with_config(&config)?;
1321    let files = scan_result.files;
1322
1323    // Process files in parallel
1324    let findings: Vec<PathTraversalFinding> = files
1325        .par_iter()
1326        .filter_map(|file| {
1327            scan_file_path_traversal(file, language).ok()
1328        })
1329        .flatten()
1330        .collect();
1331
1332    Ok(findings)
1333}
1334
1335/// Scan a single file for path traversal vulnerabilities.
1336///
1337/// # Arguments
1338///
1339/// * `file` - Path to the file to scan
1340/// * `language` - Optional language override (auto-detected if None)
1341///
1342/// # Returns
1343///
1344/// Vector of path traversal findings in this file.
1345pub fn scan_file_path_traversal(file: &Path, language: Option<&str>) -> Result<Vec<PathTraversalFinding>> {
1346    let registry = LanguageRegistry::global();
1347
1348    // Detect language
1349    let lang = match language {
1350        Some(lang_name) => registry
1351            .get_by_name(lang_name)
1352            .ok_or_else(|| BrrrError::UnsupportedLanguage(lang_name.to_string()))?,
1353        None => registry
1354            .detect_language(file)
1355            .ok_or_else(|| BrrrError::UnsupportedLanguage(
1356                file.extension()
1357                    .and_then(|e| e.to_str())
1358                    .unwrap_or("unknown")
1359                    .to_string(),
1360            ))?,
1361    };
1362
1363    let lang_name = lang.name();
1364
1365    // Get query for this language
1366    let sink_query_str = get_sink_query(lang_name)
1367        .ok_or_else(|| BrrrError::UnsupportedLanguage(format!("{} (no path traversal query)", lang_name)))?;
1368
1369    // Parse the file
1370    let source = std::fs::read(file).map_err(|e| BrrrError::io_with_path(e, file))?;
1371    let mut parser = lang.parser_for_path(file)?;
1372    let tree = parser.parse(&source, None).ok_or_else(|| BrrrError::Parse {
1373        file: file.display().to_string(),
1374        message: "Failed to parse file".to_string(),
1375    })?;
1376
1377    let ts_lang = tree.language();
1378    let file_path = file.display().to_string();
1379
1380    // Find sinks and analyze
1381    find_path_traversal_vulnerabilities(&tree, &source, &ts_lang, sink_query_str, lang_name, &file_path)
1382}
1383
1384/// Internal function to find path traversal vulnerabilities in parsed tree.
1385fn find_path_traversal_vulnerabilities(
1386    tree: &Tree,
1387    source: &[u8],
1388    ts_lang: &tree_sitter::Language,
1389    query_str: &str,
1390    lang_name: &str,
1391    file_path: &str,
1392) -> Result<Vec<PathTraversalFinding>> {
1393    let query = Query::new(ts_lang, query_str)
1394        .map_err(|e| BrrrError::TreeSitter(format_query_error(lang_name, "path_traversal", query_str, &e)))?;
1395
1396    let mut cursor = QueryCursor::new();
1397    let mut matches = cursor.matches(&query, tree.root_node(), source);
1398
1399    // Get capture indices
1400    let sink_idx = query.capture_index_for_name("sink");
1401    let join_sink_idx = query.capture_index_for_name("join_sink");
1402    let path_new_idx = query.capture_index_for_name("path_new_sink");
1403    let func_idx = query.capture_index_for_name("func");
1404    let args_idx = query.capture_index_for_name("args");
1405    let string_lit_idx = query.capture_index_for_name("string_lit");
1406    let template_lit_idx = query.capture_index_for_name("template_lit");
1407
1408    let mut findings = Vec::new();
1409    let known_sinks = get_file_sinks(lang_name);
1410
1411    // Track functions with validation patterns nearby
1412    let validation_patterns = find_validation_patterns(tree, source, lang_name);
1413
1414    while let Some(match_) = matches.next() {
1415        // Check for hardcoded "../" in string literals
1416        if let Some(idx) = string_lit_idx {
1417            if let Some(capture) = match_.captures.iter().find(|c| c.index == idx) {
1418                let text = node_text(capture.node, source);
1419                if text.contains("../") || text.contains("..\\") {
1420                    let location = node_to_location(capture.node, file_path);
1421
1422                    // Skip if this looks like a test or comment
1423                    if !is_likely_test_or_comment(capture.node, source) {
1424                        findings.push(PathTraversalFinding {
1425                            location,
1426                            severity: Severity::Medium,
1427                            sink_function: "string_literal".to_string(),
1428                            operation_type: FileOperationType::Open,
1429                            path_expression: text.to_string(),
1430                            confidence: Confidence::High,
1431                            pattern: VulnerablePattern::HardcodedTraversal,
1432                            involved_variables: vec![],
1433                            code_snippet: extract_code_snippet(source, capture.node),
1434                            description: "Hardcoded path traversal sequence '../' found. This may indicate intentional traversal or a vulnerability.".to_string(),
1435                            remediation: "Remove hardcoded '../' sequences. Use absolute paths or validate that the resolved path stays within the intended directory.".to_string(),
1436                            symlink_risk: false,
1437                        });
1438                    }
1439                }
1440            }
1441        }
1442
1443        // Check template literals for interpolation with traversal
1444        if let Some(idx) = template_lit_idx {
1445            if let Some(capture) = match_.captures.iter().find(|c| c.index == idx) {
1446                let text = node_text(capture.node, source);
1447                if (text.contains("../") || text.contains("${")) && has_substitution(capture.node) {
1448                    let location = node_to_location(capture.node, file_path);
1449                    let vars = extract_template_variables(capture.node, source);
1450
1451                    findings.push(PathTraversalFinding {
1452                        location,
1453                        severity: Severity::High,
1454                        sink_function: "template_literal".to_string(),
1455                        operation_type: FileOperationType::Open,
1456                        path_expression: text.to_string(),
1457                        confidence: Confidence::Medium,
1458                        pattern: VulnerablePattern::PathInterpolation,
1459                        involved_variables: vars,
1460                        code_snippet: extract_code_snippet(source, capture.node),
1461                        description: "Template literal with path interpolation detected. User input may enable path traversal.".to_string(),
1462                        remediation: "Validate interpolated values. Use path.basename() to extract only filename, or validate with realpath() + startswith() check.".to_string(),
1463                        symlink_risk: true,
1464                    });
1465                }
1466            }
1467        }
1468
1469        // Check for join_sink (os.path.join, path.join, filepath.Join)
1470        if let Some(idx) = join_sink_idx {
1471            if let Some(capture) = match_.captures.iter().find(|c| c.index == idx) {
1472                let call_node = capture.node;
1473                let args_node = args_idx.and_then(|i| match_.captures.iter().find(|c| c.index == i)).map(|c| c.node);
1474
1475                if let Some(args) = args_node {
1476                    let (path_expr, vars) = extract_path_argument(args, source, 1);
1477
1478                    // Check if any argument looks like user input
1479                    if looks_like_user_input(&path_expr, &vars) {
1480                        let location = node_to_location(call_node, file_path);
1481                        let func_name = get_join_function_name(lang_name);
1482
1483                        findings.push(PathTraversalFinding {
1484                            location,
1485                            severity: Severity::High,
1486                            sink_function: func_name.to_string(),
1487                            operation_type: FileOperationType::Open,
1488                            path_expression: path_expr,
1489                            confidence: Confidence::High,
1490                            pattern: VulnerablePattern::UnsafePathJoin,
1491                            involved_variables: vars,
1492                            code_snippet: extract_code_snippet(source, call_node),
1493                            description: format!("{}() with user input is NOT safe! It does not prevent absolute paths or '..' sequences.", func_name),
1494                            remediation: get_join_remediation(lang_name),
1495                            symlink_risk: true,
1496                        });
1497                    }
1498                }
1499            }
1500        }
1501
1502        // Check for regular file operation sinks
1503        if let Some(idx) = sink_idx {
1504            if let Some(capture) = match_.captures.iter().find(|c| c.index == idx) {
1505                let call_node = capture.node;
1506                let func_node = func_idx.and_then(|i| match_.captures.iter().find(|c| c.index == i)).map(|c| c.node);
1507                let args_node = args_idx.and_then(|i| match_.captures.iter().find(|c| c.index == i)).map(|c| c.node);
1508
1509                let func_name = func_node
1510                    .map(|n| node_text(n, source))
1511                    .unwrap_or("unknown");
1512
1513                // Find matching sink definition
1514                let sink_def = known_sinks.iter().find(|s| s.function == func_name || s.function.ends_with(&format!("::{}", func_name)));
1515
1516                if let Some(args) = args_node {
1517                    let path_arg_idx = sink_def.map(|s| s.path_arg_index).unwrap_or(0);
1518                    let (path_expr, vars) = extract_path_argument(args, source, path_arg_idx);
1519
1520                    // Skip if this looks like safe validation is nearby
1521                    let has_validation = check_nearby_validation(&validation_patterns, call_node, &vars);
1522
1523                    if !has_validation && !vars.is_empty() {
1524                        let operation_type = sink_def.map(|s| s.operation_type).unwrap_or(FileOperationType::Open);
1525                        let base_severity = sink_def.map(|s| s.severity).unwrap_or(Severity::Medium);
1526
1527                        let (confidence, pattern) = analyze_path_expression(&path_expr, &vars, lang_name);
1528
1529                        // Adjust severity based on confidence
1530                        let severity = if confidence == Confidence::High {
1531                            base_severity
1532                        } else if confidence == Confidence::Medium {
1533                            match base_severity {
1534                                Severity::Critical => Severity::High,
1535                                s => s,
1536                            }
1537                        } else {
1538                            Severity::Low
1539                        };
1540
1541                        let location = node_to_location(call_node, file_path);
1542
1543                        findings.push(PathTraversalFinding {
1544                            location,
1545                            severity,
1546                            sink_function: func_name.to_string(),
1547                            operation_type,
1548                            path_expression: path_expr.clone(),
1549                            confidence,
1550                            pattern,
1551                            involved_variables: vars.clone(),
1552                            code_snippet: extract_code_snippet(source, call_node),
1553                            description: generate_description(func_name, &pattern, &vars),
1554                            remediation: generate_remediation(lang_name, operation_type, &pattern),
1555                            symlink_risk: operation_type != FileOperationType::Exists,
1556                        });
1557                    }
1558                }
1559            }
1560        }
1561
1562        // Handle Path::new in Rust
1563        if let Some(idx) = path_new_idx {
1564            if let Some(capture) = match_.captures.iter().find(|c| c.index == idx) {
1565                let call_node = capture.node;
1566                let args_node = args_idx.and_then(|i| match_.captures.iter().find(|c| c.index == i)).map(|c| c.node);
1567
1568                if let Some(args) = args_node {
1569                    let (path_expr, vars) = extract_path_argument(args, source, 0);
1570
1571                    if looks_like_user_input(&path_expr, &vars) {
1572                        let location = node_to_location(call_node, file_path);
1573
1574                        findings.push(PathTraversalFinding {
1575                            location,
1576                            severity: Severity::Medium,
1577                            sink_function: "Path::new".to_string(),
1578                            operation_type: FileOperationType::Open,
1579                            path_expression: path_expr,
1580                            confidence: Confidence::Medium,
1581                            pattern: VulnerablePattern::UnvalidatedVariable,
1582                            involved_variables: vars,
1583                            code_snippet: extract_code_snippet(source, call_node),
1584                            description: "Path::new() with user input may enable path traversal when used with file operations.".to_string(),
1585                            remediation: "Validate the path after canonicalizing: use std::fs::canonicalize() and verify it starts with the expected base directory.".to_string(),
1586                            symlink_risk: true,
1587                        });
1588                    }
1589                }
1590            }
1591        }
1592    }
1593
1594    Ok(findings)
1595}
1596
1597// =============================================================================
1598// Helper Functions
1599// =============================================================================
1600
1601/// Get node text safely.
1602fn node_text<'a>(node: Node<'a>, source: &'a [u8]) -> &'a str {
1603    std::str::from_utf8(&source[node.start_byte()..node.end_byte()]).unwrap_or("")
1604}
1605
1606/// Convert node to SourceLocation.
1607fn node_to_location(node: Node, file_path: &str) -> SourceLocation {
1608    SourceLocation {
1609        file: file_path.to_string(),
1610        line: node.start_position().row + 1,
1611        column: node.start_position().column + 1,
1612        end_line: node.end_position().row + 1,
1613        end_column: node.end_position().column + 1,
1614    }
1615}
1616
1617/// Extract code snippet around a node.
1618fn extract_code_snippet(source: &[u8], node: Node) -> Option<String> {
1619    let source_str = std::str::from_utf8(source).ok()?;
1620    let lines: Vec<&str> = source_str.lines().collect();
1621
1622    let start_line = node.start_position().row;
1623    let end_line = node.end_position().row;
1624
1625    // Get 1 line before and after
1626    let context_start = start_line.saturating_sub(1);
1627    let context_end = (end_line + 2).min(lines.len());
1628
1629    let snippet: Vec<String> = lines[context_start..context_end]
1630        .iter()
1631        .enumerate()
1632        .map(|(i, line)| format!("{:4} | {}", context_start + i + 1, line))
1633        .collect();
1634
1635    Some(snippet.join("\n"))
1636}
1637
1638/// Extract the path argument from function call.
1639fn extract_path_argument(args_node: Node, source: &[u8], arg_index: usize) -> (String, Vec<String>) {
1640    let mut positional_args = Vec::new();
1641    let mut cursor = args_node.walk();
1642
1643    for child in args_node.children(&mut cursor) {
1644        match child.kind() {
1645            "(" | ")" | "," | "keyword_argument" => continue,
1646            _ => positional_args.push(child),
1647        }
1648    }
1649
1650    if let Some(arg_node) = positional_args.get(arg_index) {
1651        let text = node_text(*arg_node, source).to_string();
1652        let vars = collect_variables(*arg_node, source);
1653        (text, vars)
1654    } else if !positional_args.is_empty() {
1655        // Fallback to first arg
1656        let text = node_text(positional_args[0], source).to_string();
1657        let vars = collect_variables(positional_args[0], source);
1658        (text, vars)
1659    } else {
1660        (String::new(), Vec::new())
1661    }
1662}
1663
1664/// Collect all identifier variables from a node.
1665fn collect_variables(node: Node, source: &[u8]) -> Vec<String> {
1666    let mut vars = Vec::new();
1667    collect_variables_recursive(node, source, &mut vars);
1668    vars.sort();
1669    vars.dedup();
1670    vars
1671}
1672
1673fn collect_variables_recursive(node: Node, source: &[u8], vars: &mut Vec<String>) {
1674    if node.kind() == "identifier" {
1675        let name = node_text(node, source).to_string();
1676        // Filter out common non-user-input identifiers
1677        let ignore_list = ["True", "False", "None", "self", "cls", "os", "fs", "path", "shutil", "ioutil", "filepath", "std"];
1678        if !ignore_list.contains(&name.as_str()) && !name.is_empty() {
1679            vars.push(name);
1680        }
1681    }
1682
1683    let mut cursor = node.walk();
1684    for child in node.children(&mut cursor) {
1685        collect_variables_recursive(child, source, vars);
1686    }
1687}
1688
1689/// Check if expression looks like user input.
1690/// Uses Aho-Corasick for O(n) multi-pattern matching instead of O(n*m) sequential contains().
1691fn looks_like_user_input(expr: &str, vars: &[String]) -> bool {
1692    let lower = expr.to_lowercase();
1693    if USER_INPUT_PATTERNS.is_match(&lower) {
1694        return true;
1695    }
1696
1697    for var in vars {
1698        let lower_var = var.to_lowercase();
1699        if USER_INPUT_PATTERNS.is_match(&lower_var) {
1700            return true;
1701        }
1702    }
1703
1704    // Also flag if it's just a variable (not a literal)
1705    if !vars.is_empty() && !expr.starts_with('"') && !expr.starts_with('\'') && !expr.starts_with('`') {
1706        return true;
1707    }
1708
1709    false
1710}
1711
1712/// Analyze path expression to determine confidence and pattern.
1713fn analyze_path_expression(expr: &str, vars: &[String], _lang: &str) -> (Confidence, VulnerablePattern) {
1714    let suspicious = [
1715        "request", "req", "params", "query", "body", "input",
1716        "user", "filename", "file_name", "filepath", "file_path",
1717    ];
1718
1719    let lower = expr.to_lowercase();
1720
1721    // High confidence if direct suspicious variable
1722    for pattern in suspicious {
1723        if lower.contains(pattern) {
1724            return (Confidence::High, VulnerablePattern::DirectUserInput);
1725        }
1726    }
1727
1728    // Check for concatenation
1729    if expr.contains('+') || expr.contains("format") || expr.contains('%') {
1730        return (Confidence::Medium, VulnerablePattern::PathConcatenation);
1731    }
1732
1733    // Check for interpolation (f-strings, template literals)
1734    if expr.contains('{') && expr.contains('}') {
1735        return (Confidence::Medium, VulnerablePattern::PathInterpolation);
1736    }
1737
1738    // Check for template substitution
1739    if expr.contains("${") {
1740        return (Confidence::Medium, VulnerablePattern::PathInterpolation);
1741    }
1742
1743    // Variable without visible validation
1744    if !vars.is_empty() {
1745        return (Confidence::Low, VulnerablePattern::UnvalidatedVariable);
1746    }
1747
1748    (Confidence::Low, VulnerablePattern::MissingValidation)
1749}
1750
1751/// Find validation patterns in the code (realpath + startswith, basename, etc.)
1752fn find_validation_patterns(_tree: &Tree, source: &[u8], lang: &str) -> HashSet<String> {
1753    let mut validated_vars = HashSet::new();
1754    let source_str = std::str::from_utf8(source).unwrap_or("");
1755
1756    match lang {
1757        "python" => {
1758            // Look for realpath + startswith pattern
1759            if source_str.contains("realpath") && source_str.contains("startswith") {
1760                validated_vars.insert("_validated_".to_string());
1761            }
1762            // basename strips directory
1763            if source_str.contains("basename") {
1764                validated_vars.insert("_basename_".to_string());
1765            }
1766        }
1767        "typescript" | "javascript" => {
1768            // path.resolve + startsWith
1769            if source_str.contains("resolve") && source_str.contains("startsWith") {
1770                validated_vars.insert("_validated_".to_string());
1771            }
1772            // path.basename
1773            if source_str.contains("basename") {
1774                validated_vars.insert("_basename_".to_string());
1775            }
1776        }
1777        "go" => {
1778            // filepath.Clean + strings.HasPrefix
1779            if source_str.contains("filepath.Clean") && source_str.contains("HasPrefix") {
1780                validated_vars.insert("_validated_".to_string());
1781            }
1782            // filepath.Base
1783            if source_str.contains("filepath.Base") {
1784                validated_vars.insert("_basename_".to_string());
1785            }
1786        }
1787        "rust" => {
1788            // canonicalize + starts_with
1789            if source_str.contains("canonicalize") && source_str.contains("starts_with") {
1790                validated_vars.insert("_validated_".to_string());
1791            }
1792            // file_name() extracts filename only
1793            if source_str.contains("file_name()") {
1794                validated_vars.insert("_basename_".to_string());
1795            }
1796        }
1797        "c" | "cpp" => {
1798            // realpath + strncmp/strstr
1799            if source_str.contains("realpath") && (source_str.contains("strncmp") || source_str.contains("strstr")) {
1800                validated_vars.insert("_validated_".to_string());
1801            }
1802            // basename
1803            if source_str.contains("basename") {
1804                validated_vars.insert("_basename_".to_string());
1805            }
1806        }
1807        _ => {}
1808    }
1809
1810    validated_vars
1811}
1812
1813/// Check if there's validation nearby for the given variables.
1814fn check_nearby_validation(validation_patterns: &HashSet<String>, _node: Node, _vars: &[String]) -> bool {
1815    // If we detected validation patterns in the file, lower confidence
1816    // This is a heuristic - could be improved with actual data flow tracking
1817    !validation_patterns.is_empty()
1818}
1819
1820/// Check if node is likely in test code or a comment.
1821fn is_likely_test_or_comment(node: Node, source: &[u8]) -> bool {
1822    // Walk up to find function/class context
1823    let mut current = Some(node);
1824    while let Some(n) = current {
1825        let text = node_text(n, source).to_lowercase();
1826        if text.contains("test") || text.contains("mock") || text.contains("spec") {
1827            return true;
1828        }
1829        current = n.parent();
1830    }
1831    false
1832}
1833
1834/// Check if template string has substitutions.
1835fn has_substitution(node: Node) -> bool {
1836    let mut cursor = node.walk();
1837    for child in node.children(&mut cursor) {
1838        if child.kind() == "template_substitution" {
1839            return true;
1840        }
1841    }
1842    false
1843}
1844
1845/// Extract variables from template literal substitutions.
1846fn extract_template_variables(node: Node, source: &[u8]) -> Vec<String> {
1847    let mut vars = Vec::new();
1848    let mut cursor = node.walk();
1849
1850    for child in node.children(&mut cursor) {
1851        if child.kind() == "template_substitution" {
1852            vars.extend(collect_variables(child, source));
1853        }
1854    }
1855
1856    vars
1857}
1858
1859/// Get the join function name for a language.
1860fn get_join_function_name(lang: &str) -> &'static str {
1861    match lang {
1862        "python" => "os.path.join",
1863        "typescript" | "javascript" => "path.join",
1864        "go" => "filepath.Join",
1865        _ => "path.join",
1866    }
1867}
1868
1869/// Get remediation for unsafe path join usage.
1870fn get_join_remediation(lang: &str) -> String {
1871    match lang {
1872        "python" => {
1873            "os.path.join() does NOT sanitize user input! Fix:\n\
1874             1. Use os.path.basename() to extract only filename: safe_name = os.path.basename(user_input)\n\
1875             2. Or validate with realpath: resolved = os.path.realpath(os.path.join(base, user_input))\n\
1876                if not resolved.startswith(os.path.realpath(base)):\n\
1877                    raise ValueError('Path traversal detected')".to_string()
1878        }
1879        "typescript" | "javascript" => {
1880            "path.join() does NOT sanitize user input! Fix:\n\
1881             1. Use path.basename() to extract only filename: const safeName = path.basename(userInput)\n\
1882             2. Or validate: const resolved = path.resolve(base, userInput)\n\
1883                if (!resolved.startsWith(path.resolve(base))) throw new Error('Path traversal')".to_string()
1884        }
1885        "go" => {
1886            "filepath.Join() does NOT sanitize user input! Fix:\n\
1887             1. Use filepath.Base() to extract only filename: safeName := filepath.Base(userInput)\n\
1888             2. Or validate: resolved := filepath.Clean(filepath.Join(base, userInput))\n\
1889                if !strings.HasPrefix(resolved, filepath.Clean(base)) { return error }".to_string()
1890        }
1891        _ => "Path join functions do NOT sanitize input. Use basename() or validate the resolved path.".to_string(),
1892    }
1893}
1894
1895/// Generate human-readable description.
1896fn generate_description(func_name: &str, pattern: &VulnerablePattern, vars: &[String]) -> String {
1897    let var_list = if vars.is_empty() {
1898        "unknown variable".to_string()
1899    } else {
1900        vars.join(", ")
1901    };
1902
1903    match pattern {
1904        VulnerablePattern::DirectUserInput => {
1905            format!(
1906                "Potential path traversal in {}(). Variable '{}' appears to be user-controlled and is passed directly to file operation.",
1907                func_name, var_list
1908            )
1909        }
1910        VulnerablePattern::UnsafePathJoin => {
1911            format!(
1912                "Unsafe path join in {}() with user input '{}'. Path join functions do NOT prevent path traversal!",
1913                func_name, var_list
1914            )
1915        }
1916        VulnerablePattern::PathConcatenation => {
1917            format!(
1918                "Path concatenation in {}() with variable '{}'. String concatenation for paths is vulnerable to traversal.",
1919                func_name, var_list
1920            )
1921        }
1922        VulnerablePattern::HardcodedTraversal => {
1923            "Hardcoded '../' path traversal sequence detected. This may allow escaping the intended directory.".to_string()
1924        }
1925        VulnerablePattern::UnvalidatedVariable => {
1926            format!(
1927                "Variable '{}' passed to {}() without visible validation. May enable path traversal if user-controlled.",
1928                var_list, func_name
1929            )
1930        }
1931        VulnerablePattern::PathInterpolation => {
1932            format!(
1933                "Path interpolation in {}() with variables '{}'. Interpolated paths are vulnerable to traversal attacks.",
1934                func_name, var_list
1935            )
1936        }
1937        VulnerablePattern::MissingValidation => {
1938            format!(
1939                "File operation {}() without visible path validation. Ensure paths are validated before use.",
1940                func_name
1941            )
1942        }
1943    }
1944}
1945
1946/// Generate remediation advice.
1947fn generate_remediation(lang: &str, operation_type: FileOperationType, pattern: &VulnerablePattern) -> String {
1948    let op_warning = match operation_type {
1949        FileOperationType::Write | FileOperationType::Append => "WARNING: Write operation - attackers could overwrite critical files!",
1950        FileOperationType::Delete => "CRITICAL: Delete operation - attackers could delete arbitrary files!",
1951        FileOperationType::Read => "Attackers could read sensitive files like /etc/passwd or config files.",
1952        FileOperationType::Copy | FileOperationType::Move => "Attackers could copy/move files to/from unintended locations.",
1953        _ => "Attackers could access files outside the intended directory.",
1954    };
1955
1956    let fix = match lang {
1957        "python" => {
1958            "Fix for Python:\n\
1959             1. Extract filename only: safe_name = os.path.basename(user_input)\n\
1960             2. Or validate resolved path:\n\
1961                base = os.path.realpath('/safe/base/dir')\n\
1962                resolved = os.path.realpath(os.path.join(base, user_input))\n\
1963                if not resolved.startswith(base + os.sep):\n\
1964                    raise ValueError('Path traversal attempt')"
1965        }
1966        "typescript" | "javascript" => {
1967            "Fix for JavaScript/TypeScript:\n\
1968             1. Extract filename only: const safeName = path.basename(userInput)\n\
1969             2. Or validate resolved path:\n\
1970                const base = path.resolve('/safe/base/dir')\n\
1971                const resolved = path.resolve(base, userInput)\n\
1972                if (!resolved.startsWith(base + path.sep)) {\n\
1973                    throw new Error('Path traversal attempt')\n\
1974                }"
1975        }
1976        "go" => {
1977            "Fix for Go:\n\
1978             1. Extract filename only: safeName := filepath.Base(userInput)\n\
1979             2. Or validate resolved path:\n\
1980                base, _ := filepath.Abs(\"/safe/base/dir\")\n\
1981                resolved := filepath.Clean(filepath.Join(base, userInput))\n\
1982                if !strings.HasPrefix(resolved, base + string(os.PathSeparator)) {\n\
1983                    return errors.New(\"path traversal attempt\")\n\
1984                }"
1985        }
1986        "rust" => {
1987            "Fix for Rust:\n\
1988             1. Extract filename only: let safe_name = Path::new(user_input).file_name()\n\
1989             2. Or validate canonical path:\n\
1990                let base = std::fs::canonicalize(\"/safe/base/dir\")?;\n\
1991                let resolved = std::fs::canonicalize(base.join(user_input))?;\n\
1992                if !resolved.starts_with(&base) {\n\
1993                    return Err(\"path traversal attempt\")\n\
1994                }"
1995        }
1996        "c" | "cpp" => {
1997            "Fix for C/C++:\n\
1998             1. Extract filename only: char *safe_name = basename(user_input)\n\
1999             2. Or validate with realpath:\n\
2000                char base[PATH_MAX], resolved[PATH_MAX];\n\
2001                realpath(\"/safe/base/dir\", base);\n\
2002                realpath(combined_path, resolved);\n\
2003                if (strncmp(resolved, base, strlen(base)) != 0) {\n\
2004                    // Path traversal detected\n\
2005                }"
2006        }
2007        _ => "Use basename() to extract only filename, or validate that the resolved absolute path starts with the expected base directory.",
2008    };
2009
2010    let symlink_warning = match pattern {
2011        VulnerablePattern::HardcodedTraversal => "",
2012        _ => "\n\nSymlink Warning: Even with validation, symlink attacks may be possible. Consider:\n\
2013              - Use O_NOFOLLOW flag (POSIX) to prevent symlink following\n\
2014              - Validate paths at the moment of use (TOCTOU protection)\n\
2015              - Use chroot or containerization for stronger isolation"
2016    };
2017
2018    format!("{}\n\n{}{}", op_warning, fix, symlink_warning)
2019}
2020
2021// =============================================================================
2022// Tests
2023// =============================================================================
2024
2025#[cfg(test)]
2026mod tests {
2027    use super::*;
2028    use std::io::Write;
2029    use tempfile::NamedTempFile;
2030
2031    fn create_temp_file(content: &str, extension: &str) -> NamedTempFile {
2032        let mut file = tempfile::Builder::new()
2033            .suffix(extension)
2034            .tempfile()
2035            .expect("Failed to create temp file");
2036        file.write_all(content.as_bytes()).expect("Failed to write");
2037        file
2038    }
2039
2040    // =========================================================================
2041    // Python Tests
2042    // =========================================================================
2043
2044    #[test]
2045    fn test_python_direct_open_user_input() {
2046        let source = r#"
2047def read_file(request):
2048    filename = request.args.get('filename')
2049    with open(filename) as f:
2050        return f.read()
2051"#;
2052        let file = create_temp_file(source, ".py");
2053        let findings = scan_file_path_traversal(file.path(), Some("python"))
2054            .expect("Scan should succeed");
2055
2056        assert!(!findings.is_empty(), "Should detect open() with user input");
2057        let finding = &findings[0];
2058        assert_eq!(finding.sink_function, "open");
2059        assert!(finding.severity >= Severity::Medium);
2060    }
2061
2062    #[test]
2063    fn test_python_unsafe_path_join() {
2064        let source = r#"
2065import os
2066
2067def download_file(user_filename):
2068    base_dir = "/var/www/uploads"
2069    filepath = os.path.join(base_dir, user_filename)  # NOT SAFE!
2070    with open(filepath, 'rb') as f:
2071        return f.read()
2072"#;
2073        let file = create_temp_file(source, ".py");
2074        let findings = scan_file_path_traversal(file.path(), Some("python"))
2075            .expect("Scan should succeed");
2076
2077        // Should flag os.path.join or the open()
2078        assert!(!findings.is_empty(), "Should detect path traversal risk");
2079    }
2080
2081    #[test]
2082    fn test_python_hardcoded_traversal() {
2083        let source = r#"
2084def get_parent_config():
2085    with open("../config.ini") as f:
2086        return f.read()
2087"#;
2088        let file = create_temp_file(source, ".py");
2089        let findings = scan_file_path_traversal(file.path(), Some("python"))
2090            .expect("Scan should succeed");
2091
2092        let traversal_finding = findings.iter()
2093            .find(|f| f.pattern == VulnerablePattern::HardcodedTraversal);
2094        assert!(traversal_finding.is_some(), "Should detect hardcoded '../'");
2095    }
2096
2097    #[test]
2098    fn test_python_shutil_rmtree() {
2099        let source = r#"
2100import shutil
2101
2102def delete_user_folder(user_path):
2103    shutil.rmtree(user_path)  # Critical!
2104"#;
2105        let file = create_temp_file(source, ".py");
2106        let findings = scan_file_path_traversal(file.path(), Some("python"))
2107            .expect("Scan should succeed");
2108
2109        assert!(!findings.is_empty(), "Should detect shutil.rmtree");
2110        // Should be high/critical severity for delete operations
2111        let finding = findings.iter().find(|f| f.sink_function == "rmtree");
2112        assert!(finding.is_some() || !findings.is_empty());
2113    }
2114
2115    // =========================================================================
2116    // TypeScript Tests
2117    // =========================================================================
2118
2119    #[test]
2120    fn test_typescript_fs_readfile() {
2121        let source = r#"
2122import * as fs from 'fs';
2123
2124function readUserFile(req: Request) {
2125    const filename = req.params.filename;
2126    return fs.readFileSync(filename);
2127}
2128"#;
2129        let file = create_temp_file(source, ".ts");
2130        let findings = scan_file_path_traversal(file.path(), Some("typescript"))
2131            .expect("Scan should succeed");
2132
2133        assert!(!findings.is_empty(), "Should detect fs.readFileSync with user input");
2134    }
2135
2136    #[test]
2137    fn test_typescript_path_join() {
2138        let source = r#"
2139import * as fs from 'fs';
2140import * as path from 'path';
2141
2142function getFile(userPath: string) {
2143    const fullPath = path.join('/uploads', userPath);  // NOT SAFE!
2144    return fs.readFileSync(fullPath);
2145}
2146"#;
2147        let file = create_temp_file(source, ".ts");
2148        let findings = scan_file_path_traversal(file.path(), Some("typescript"))
2149            .expect("Scan should succeed");
2150
2151        assert!(!findings.is_empty(), "Should detect path.join vulnerability");
2152    }
2153
2154    #[test]
2155    fn test_typescript_template_literal() {
2156        let source = r#"
2157import * as fs from 'fs';
2158
2159function readConfig(userId: string) {
2160    const path = `/data/${userId}/config.json`;
2161    return fs.readFileSync(path);
2162}
2163"#;
2164        let file = create_temp_file(source, ".ts");
2165        let findings = scan_file_path_traversal(file.path(), Some("typescript"))
2166            .expect("Scan should succeed");
2167
2168        // May detect template literal interpolation
2169        if !findings.is_empty() {
2170            println!("Found {} findings", findings.len());
2171        }
2172    }
2173
2174    // =========================================================================
2175    // Go Tests
2176    // =========================================================================
2177
2178    #[test]
2179    fn test_go_os_open() {
2180        let source = r#"
2181package main
2182
2183import "os"
2184
2185func readFile(userPath string) ([]byte, error) {
2186    f, err := os.Open(userPath)
2187    if err != nil {
2188        return nil, err
2189    }
2190    defer f.Close()
2191    return io.ReadAll(f)
2192}
2193"#;
2194        let file = create_temp_file(source, ".go");
2195        let findings = scan_file_path_traversal(file.path(), Some("go"))
2196            .expect("Scan should succeed");
2197
2198        assert!(!findings.is_empty(), "Should detect os.Open with user path");
2199    }
2200
2201    #[test]
2202    fn test_go_filepath_join() {
2203        let source = r#"
2204package main
2205
2206import (
2207    "os"
2208    "path/filepath"
2209)
2210
2211func getFile(basePath, userInput string) ([]byte, error) {
2212    path := filepath.Join(basePath, userInput)  // NOT SAFE!
2213    return os.ReadFile(path)
2214}
2215"#;
2216        let file = create_temp_file(source, ".go");
2217        let findings = scan_file_path_traversal(file.path(), Some("go"))
2218            .expect("Scan should succeed");
2219
2220        assert!(!findings.is_empty(), "Should detect filepath.Join vulnerability");
2221    }
2222
2223    // =========================================================================
2224    // Rust Tests
2225    // =========================================================================
2226
2227    #[test]
2228    fn test_rust_fs_read() {
2229        let source = r#"
2230use std::fs;
2231
2232fn read_user_file(user_path: &str) -> std::io::Result<String> {
2233    fs::read_to_string(user_path)
2234}
2235"#;
2236        let file = create_temp_file(source, ".rs");
2237        let findings = scan_file_path_traversal(file.path(), Some("rust"))
2238            .expect("Scan should succeed");
2239
2240        // Note: Rust detection depends on tree-sitter-rust grammar
2241        // This test verifies the scan completes
2242        println!("Rust findings: {:?}", findings);
2243    }
2244
2245    // =========================================================================
2246    // C Tests
2247    // =========================================================================
2248
2249    #[test]
2250    fn test_c_fopen() {
2251        let source = r#"
2252#include <stdio.h>
2253
2254void read_file(const char* user_path) {
2255    FILE* f = fopen(user_path, "r");
2256    if (f) {
2257        // read file
2258        fclose(f);
2259    }
2260}
2261"#;
2262        let file = create_temp_file(source, ".c");
2263        let findings = scan_file_path_traversal(file.path(), Some("c"))
2264            .expect("Scan should succeed");
2265
2266        assert!(!findings.is_empty(), "Should detect fopen with user path");
2267        assert_eq!(findings[0].sink_function, "fopen");
2268    }
2269
2270    #[test]
2271    fn test_c_hardcoded_traversal() {
2272        let source = r#"
2273#include <stdio.h>
2274
2275void read_parent() {
2276    FILE* f = fopen("../secret.txt", "r");
2277    fclose(f);
2278}
2279"#;
2280        let file = create_temp_file(source, ".c");
2281        let findings = scan_file_path_traversal(file.path(), Some("c"))
2282            .expect("Scan should succeed");
2283
2284        let traversal = findings.iter()
2285            .find(|f| f.pattern == VulnerablePattern::HardcodedTraversal);
2286        assert!(traversal.is_some(), "Should detect hardcoded '../'");
2287    }
2288
2289    // =========================================================================
2290    // Utility Tests
2291    // =========================================================================
2292
2293    #[test]
2294    fn test_severity_ordering() {
2295        assert!(Severity::Critical > Severity::High);
2296        assert!(Severity::High > Severity::Medium);
2297        assert!(Severity::Medium > Severity::Low);
2298        assert!(Severity::Low > Severity::Info);
2299    }
2300
2301    #[test]
2302    fn test_confidence_ordering() {
2303        assert!(Confidence::High > Confidence::Medium);
2304        assert!(Confidence::Medium > Confidence::Low);
2305    }
2306
2307    #[test]
2308    fn test_file_operation_type_display() {
2309        assert_eq!(format!("{}", FileOperationType::Read), "read");
2310        assert_eq!(format!("{}", FileOperationType::Write), "write");
2311        assert_eq!(format!("{}", FileOperationType::Delete), "delete");
2312    }
2313
2314    #[test]
2315    fn test_vulnerable_pattern_display() {
2316        assert_eq!(format!("{}", VulnerablePattern::UnsafePathJoin), "unsafe_path_join");
2317        assert_eq!(format!("{}", VulnerablePattern::HardcodedTraversal), "hardcoded_traversal");
2318    }
2319
2320    #[test]
2321    fn test_looks_like_user_input() {
2322        assert!(looks_like_user_input("request.args.get('file')", &[]));
2323        assert!(looks_like_user_input("user_filename", &["user_filename".to_string()]));
2324        assert!(looks_like_user_input("filepath", &["filepath".to_string()]));
2325        assert!(!looks_like_user_input("\"static.txt\"", &[]));
2326    }
2327
2328    #[test]
2329    fn test_get_file_sinks_coverage() {
2330        let languages = ["python", "typescript", "javascript", "go", "rust", "c", "cpp"];
2331        for lang in languages {
2332            let sinks = get_file_sinks(lang);
2333            assert!(!sinks.is_empty(), "Should have sinks for {}", lang);
2334        }
2335    }
2336}