go_brrr/security/injection/
command.rs

1//! Command injection vulnerability detection.
2//!
3//! Detects potential command injection vulnerabilities by tracking data flow
4//! from user-controlled inputs (sources) to dangerous shell execution functions (sinks).
5//!
6//! # Command Injection vs Argument Injection
7//!
8//! - **Command Injection**: User input containing shell metacharacters (`;`, `|`, `&`, etc.)
9//!   can execute arbitrary commands. Example: `os.system("ls " + user_input)` where
10//!   `user_input = "; rm -rf /"`.
11//!
12//! - **Argument Injection**: User input is passed as arguments to a command but can
13//!   manipulate flags/options. Example: `subprocess.run(["tar", "-xf", user_input])`
14//!   where `user_input = "--checkpoint-action=exec=malicious.sh"`.
15//!
16//! # Detection Strategy
17//!
18//! 1. Parse source files using tree-sitter
19//! 2. Identify calls to known command execution sinks
20//! 3. Extract the arguments passed to these sinks
21//! 4. Track data flow backwards to identify if arguments come from taint sources
22//! 5. Report findings with severity and confidence levels
23//!
24//! # Shell Metacharacters
25//!
26//! The following characters are dangerous in shell contexts:
27//! - Command separators: `;`, `|`, `&`, `&&`, `||`, `\n`
28//! - Subshell/substitution: `` ` ``, `$()`, `$()`
29//! - Redirection: `<`, `>`, `>>`
30//! - Glob expansion: `*`, `?`, `[`, `]`
31//! - Quote escape: `'`, `"`, `\`
32
33use std::collections::HashMap;
34use std::path::Path;
35
36use rayon::prelude::*;
37use serde::{Deserialize, Serialize};
38use streaming_iterator::StreamingIterator;
39use tree_sitter::{Node, Query, QueryCursor, Tree};
40
41use crate::callgraph::scanner::{ProjectScanner, ScanConfig};
42use crate::error::{Result, BrrrError};
43use crate::lang::LanguageRegistry;
44use crate::util::format_query_error;
45
46// =============================================================================
47// Type Definitions
48// =============================================================================
49
50/// Severity level for security findings.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
52#[serde(rename_all = "lowercase")]
53pub enum Severity {
54    /// Informational - may not be exploitable but worth reviewing
55    Info,
56    /// Low severity - limited impact or requires specific conditions
57    Low,
58    /// Medium severity - potential for significant impact
59    Medium,
60    /// High severity - likely exploitable with serious impact
61    High,
62    /// Critical - easily exploitable with severe consequences
63    Critical,
64}
65
66impl std::fmt::Display for Severity {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        match self {
69            Self::Info => write!(f, "INFO"),
70            Self::Low => write!(f, "LOW"),
71            Self::Medium => write!(f, "MEDIUM"),
72            Self::High => write!(f, "HIGH"),
73            Self::Critical => write!(f, "CRITICAL"),
74        }
75    }
76}
77
78/// Confidence level for the finding.
79#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
80#[serde(rename_all = "lowercase")]
81pub enum Confidence {
82    /// Low confidence - pattern match only, no data flow confirmation
83    Low,
84    /// Medium confidence - some data flow indicators but incomplete path
85    Medium,
86    /// High confidence - clear data flow from source to sink
87    High,
88}
89
90impl std::fmt::Display for Confidence {
91    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        match self {
93            Self::Low => write!(f, "LOW"),
94            Self::Medium => write!(f, "MEDIUM"),
95            Self::High => write!(f, "HIGH"),
96        }
97    }
98}
99
100/// Type of injection vulnerability.
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
102#[serde(rename_all = "snake_case")]
103pub enum InjectionKind {
104    /// Full command injection - user input can execute arbitrary commands
105    CommandInjection,
106    /// Argument injection - user input can manipulate command arguments
107    ArgumentInjection,
108    /// Code injection via eval/exec
109    CodeInjection,
110}
111
112impl std::fmt::Display for InjectionKind {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        match self {
115            Self::CommandInjection => write!(f, "command_injection"),
116            Self::ArgumentInjection => write!(f, "argument_injection"),
117            Self::CodeInjection => write!(f, "code_injection"),
118        }
119    }
120}
121
122/// Source location in code.
123#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
124pub struct SourceLocation {
125    /// File path
126    pub file: String,
127    /// Line number (1-indexed)
128    pub line: usize,
129    /// Column number (1-indexed)
130    pub column: usize,
131    /// End line number (1-indexed)
132    pub end_line: usize,
133    /// End column number (1-indexed)
134    pub end_column: usize,
135}
136
137impl std::fmt::Display for SourceLocation {
138    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139        write!(f, "{}:{}:{}", self.file, self.line, self.column)
140    }
141}
142
143/// Kind of taint source.
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
145#[serde(rename_all = "snake_case")]
146pub enum TaintSourceKind {
147    /// HTTP request parameters (query string, body, headers)
148    HttpRequest,
149    /// Form input data
150    FormInput,
151    /// Standard input (stdin)
152    StdIn,
153    /// File read operations
154    FileRead,
155    /// Environment variables
156    EnvVar,
157    /// Command line arguments
158    CmdLineArg,
159    /// Database query results
160    DatabaseResult,
161    /// Network socket data
162    NetworkData,
163    /// User-provided configuration
164    UserConfig,
165    /// Unknown/generic user input
166    Unknown,
167}
168
169/// A taint source - origin of potentially malicious data.
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct TaintSource {
172    /// Type of taint source
173    pub kind: TaintSourceKind,
174    /// Variable name carrying the taint
175    pub variable: String,
176    /// Location where taint originates
177    pub location: SourceLocation,
178    /// Description of the source
179    pub description: String,
180}
181
182/// A command execution sink - dangerous function that executes commands.
183#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct CommandSink {
185    /// Language this sink applies to
186    pub language: String,
187    /// Module or namespace (e.g., "os", "subprocess", "child_process")
188    pub module: Option<String>,
189    /// Function name (e.g., "system", "exec", "popen")
190    pub function: String,
191    /// Argument index that receives the command (0-indexed)
192    pub command_arg_index: usize,
193    /// Whether this sink uses a shell by default
194    pub shell_by_default: bool,
195    /// Severity when this sink is exploited
196    pub severity: Severity,
197    /// Description of the sink
198    pub description: String,
199}
200
201/// A command injection finding.
202#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct CommandInjectionFinding {
204    /// Location of the vulnerable sink call
205    pub location: SourceLocation,
206    /// Severity of the vulnerability
207    pub severity: Severity,
208    /// Name of the dangerous function being called
209    pub sink_function: String,
210    /// The tainted input reaching the sink (variable name or expression)
211    pub tainted_input: String,
212    /// Confidence level of the finding
213    pub confidence: Confidence,
214    /// Type of injection
215    pub kind: InjectionKind,
216    /// Chain of taint propagation (source -> ... -> sink)
217    #[serde(skip_serializing_if = "Vec::is_empty")]
218    pub taint_chain: Vec<TaintSource>,
219    /// Code snippet showing the vulnerable pattern
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub code_snippet: Option<String>,
222    /// Remediation advice
223    pub remediation: String,
224}
225
226// =============================================================================
227// Language-Specific Command Sinks
228// =============================================================================
229
230/// Get all known command execution sinks for a language.
231pub fn get_command_sinks(language: &str) -> Vec<CommandSink> {
232    match language {
233        "python" => python_sinks(),
234        "typescript" | "javascript" => typescript_sinks(),
235        "rust" => rust_sinks(),
236        "go" => go_sinks(),
237        "c" | "cpp" => c_sinks(),
238        "java" => java_sinks(),
239        _ => vec![],
240    }
241}
242
243fn python_sinks() -> Vec<CommandSink> {
244    vec![
245        // os module
246        CommandSink {
247            language: "python".to_string(),
248            module: Some("os".to_string()),
249            function: "system".to_string(),
250            command_arg_index: 0,
251            shell_by_default: true,
252            severity: Severity::Critical,
253            description: "Executes command in shell, vulnerable to command injection".to_string(),
254        },
255        CommandSink {
256            language: "python".to_string(),
257            module: Some("os".to_string()),
258            function: "popen".to_string(),
259            command_arg_index: 0,
260            shell_by_default: true,
261            severity: Severity::Critical,
262            description: "Opens pipe to command in shell".to_string(),
263        },
264        CommandSink {
265            language: "python".to_string(),
266            module: Some("os".to_string()),
267            function: "spawn".to_string(),
268            command_arg_index: 1,
269            shell_by_default: false,
270            severity: Severity::High,
271            description: "Spawns process, less dangerous but still risky".to_string(),
272        },
273        CommandSink {
274            language: "python".to_string(),
275            module: Some("os".to_string()),
276            function: "spawnl".to_string(),
277            command_arg_index: 1,
278            shell_by_default: false,
279            severity: Severity::High,
280            description: "Spawns process with list args".to_string(),
281        },
282        CommandSink {
283            language: "python".to_string(),
284            module: Some("os".to_string()),
285            function: "spawnle".to_string(),
286            command_arg_index: 1,
287            shell_by_default: false,
288            severity: Severity::High,
289            description: "Spawns process with list args and env".to_string(),
290        },
291        CommandSink {
292            language: "python".to_string(),
293            module: Some("os".to_string()),
294            function: "spawnlp".to_string(),
295            command_arg_index: 1,
296            shell_by_default: false,
297            severity: Severity::High,
298            description: "Spawns process using PATH".to_string(),
299        },
300        CommandSink {
301            language: "python".to_string(),
302            module: Some("os".to_string()),
303            function: "spawnlpe".to_string(),
304            command_arg_index: 1,
305            shell_by_default: false,
306            severity: Severity::High,
307            description: "Spawns process using PATH with env".to_string(),
308        },
309        CommandSink {
310            language: "python".to_string(),
311            module: Some("os".to_string()),
312            function: "spawnv".to_string(),
313            command_arg_index: 1,
314            shell_by_default: false,
315            severity: Severity::High,
316            description: "Spawns process with vector args".to_string(),
317        },
318        CommandSink {
319            language: "python".to_string(),
320            module: Some("os".to_string()),
321            function: "spawnve".to_string(),
322            command_arg_index: 1,
323            shell_by_default: false,
324            severity: Severity::High,
325            description: "Spawns process with vector args and env".to_string(),
326        },
327        CommandSink {
328            language: "python".to_string(),
329            module: Some("os".to_string()),
330            function: "spawnvp".to_string(),
331            command_arg_index: 1,
332            shell_by_default: false,
333            severity: Severity::High,
334            description: "Spawns process with vector args using PATH".to_string(),
335        },
336        CommandSink {
337            language: "python".to_string(),
338            module: Some("os".to_string()),
339            function: "spawnvpe".to_string(),
340            command_arg_index: 1,
341            shell_by_default: false,
342            severity: Severity::High,
343            description: "Spawns process with vector args and env using PATH".to_string(),
344        },
345        CommandSink {
346            language: "python".to_string(),
347            module: Some("os".to_string()),
348            function: "execl".to_string(),
349            command_arg_index: 0,
350            shell_by_default: false,
351            severity: Severity::High,
352            description: "Replaces current process with new program".to_string(),
353        },
354        CommandSink {
355            language: "python".to_string(),
356            module: Some("os".to_string()),
357            function: "execle".to_string(),
358            command_arg_index: 0,
359            shell_by_default: false,
360            severity: Severity::High,
361            description: "Replaces current process with env".to_string(),
362        },
363        CommandSink {
364            language: "python".to_string(),
365            module: Some("os".to_string()),
366            function: "execlp".to_string(),
367            command_arg_index: 0,
368            shell_by_default: false,
369            severity: Severity::High,
370            description: "Replaces current process using PATH".to_string(),
371        },
372        CommandSink {
373            language: "python".to_string(),
374            module: Some("os".to_string()),
375            function: "execlpe".to_string(),
376            command_arg_index: 0,
377            shell_by_default: false,
378            severity: Severity::High,
379            description: "Replaces current process using PATH with env".to_string(),
380        },
381        CommandSink {
382            language: "python".to_string(),
383            module: Some("os".to_string()),
384            function: "execv".to_string(),
385            command_arg_index: 0,
386            shell_by_default: false,
387            severity: Severity::High,
388            description: "Replaces current process with vector args".to_string(),
389        },
390        CommandSink {
391            language: "python".to_string(),
392            module: Some("os".to_string()),
393            function: "execve".to_string(),
394            command_arg_index: 0,
395            shell_by_default: false,
396            severity: Severity::High,
397            description: "Replaces current process with vector args and env".to_string(),
398        },
399        CommandSink {
400            language: "python".to_string(),
401            module: Some("os".to_string()),
402            function: "execvp".to_string(),
403            command_arg_index: 0,
404            shell_by_default: false,
405            severity: Severity::High,
406            description: "Replaces current process using PATH".to_string(),
407        },
408        CommandSink {
409            language: "python".to_string(),
410            module: Some("os".to_string()),
411            function: "execvpe".to_string(),
412            command_arg_index: 0,
413            shell_by_default: false,
414            severity: Severity::High,
415            description: "Replaces current process using PATH with env".to_string(),
416        },
417        // subprocess module
418        CommandSink {
419            language: "python".to_string(),
420            module: Some("subprocess".to_string()),
421            function: "call".to_string(),
422            command_arg_index: 0,
423            shell_by_default: false,
424            severity: Severity::High,
425            description: "Runs command, dangerous with shell=True".to_string(),
426        },
427        CommandSink {
428            language: "python".to_string(),
429            module: Some("subprocess".to_string()),
430            function: "run".to_string(),
431            command_arg_index: 0,
432            shell_by_default: false,
433            severity: Severity::High,
434            description: "Runs command, dangerous with shell=True".to_string(),
435        },
436        CommandSink {
437            language: "python".to_string(),
438            module: Some("subprocess".to_string()),
439            function: "Popen".to_string(),
440            command_arg_index: 0,
441            shell_by_default: false,
442            severity: Severity::High,
443            description: "Creates subprocess, dangerous with shell=True".to_string(),
444        },
445        CommandSink {
446            language: "python".to_string(),
447            module: Some("subprocess".to_string()),
448            function: "check_call".to_string(),
449            command_arg_index: 0,
450            shell_by_default: false,
451            severity: Severity::High,
452            description: "Runs command with return code check".to_string(),
453        },
454        CommandSink {
455            language: "python".to_string(),
456            module: Some("subprocess".to_string()),
457            function: "check_output".to_string(),
458            command_arg_index: 0,
459            shell_by_default: false,
460            severity: Severity::High,
461            description: "Runs command and captures output".to_string(),
462        },
463        CommandSink {
464            language: "python".to_string(),
465            module: Some("subprocess".to_string()),
466            function: "getoutput".to_string(),
467            command_arg_index: 0,
468            shell_by_default: true,
469            severity: Severity::Critical,
470            description: "Runs command in shell, always uses shell".to_string(),
471        },
472        CommandSink {
473            language: "python".to_string(),
474            module: Some("subprocess".to_string()),
475            function: "getstatusoutput".to_string(),
476            command_arg_index: 0,
477            shell_by_default: true,
478            severity: Severity::Critical,
479            description: "Runs command in shell, returns status and output".to_string(),
480        },
481        // commands module (deprecated but still used)
482        CommandSink {
483            language: "python".to_string(),
484            module: Some("commands".to_string()),
485            function: "getoutput".to_string(),
486            command_arg_index: 0,
487            shell_by_default: true,
488            severity: Severity::Critical,
489            description: "Deprecated shell command execution".to_string(),
490        },
491        CommandSink {
492            language: "python".to_string(),
493            module: Some("commands".to_string()),
494            function: "getstatusoutput".to_string(),
495            command_arg_index: 0,
496            shell_by_default: true,
497            severity: Severity::Critical,
498            description: "Deprecated shell command with status".to_string(),
499        },
500        // eval/exec - code injection
501        CommandSink {
502            language: "python".to_string(),
503            module: None,
504            function: "eval".to_string(),
505            command_arg_index: 0,
506            shell_by_default: false,
507            severity: Severity::Critical,
508            description: "Evaluates Python expression, code injection risk".to_string(),
509        },
510        CommandSink {
511            language: "python".to_string(),
512            module: None,
513            function: "exec".to_string(),
514            command_arg_index: 0,
515            shell_by_default: false,
516            severity: Severity::Critical,
517            description: "Executes Python code, code injection risk".to_string(),
518        },
519        CommandSink {
520            language: "python".to_string(),
521            module: None,
522            function: "compile".to_string(),
523            command_arg_index: 0,
524            shell_by_default: false,
525            severity: Severity::High,
526            description: "Compiles Python code, potential code injection".to_string(),
527        },
528        // pty module
529        CommandSink {
530            language: "python".to_string(),
531            module: Some("pty".to_string()),
532            function: "spawn".to_string(),
533            command_arg_index: 0,
534            shell_by_default: false,
535            severity: Severity::High,
536            description: "Spawns process in pseudo-terminal".to_string(),
537        },
538    ]
539}
540
541fn typescript_sinks() -> Vec<CommandSink> {
542    vec![
543        // child_process module
544        CommandSink {
545            language: "typescript".to_string(),
546            module: Some("child_process".to_string()),
547            function: "exec".to_string(),
548            command_arg_index: 0,
549            shell_by_default: true,
550            severity: Severity::Critical,
551            description: "Executes command in shell".to_string(),
552        },
553        CommandSink {
554            language: "typescript".to_string(),
555            module: Some("child_process".to_string()),
556            function: "execSync".to_string(),
557            command_arg_index: 0,
558            shell_by_default: true,
559            severity: Severity::Critical,
560            description: "Synchronously executes command in shell".to_string(),
561        },
562        CommandSink {
563            language: "typescript".to_string(),
564            module: Some("child_process".to_string()),
565            function: "spawn".to_string(),
566            command_arg_index: 0,
567            shell_by_default: false,
568            severity: Severity::High,
569            description: "Spawns process, dangerous with shell:true option".to_string(),
570        },
571        CommandSink {
572            language: "typescript".to_string(),
573            module: Some("child_process".to_string()),
574            function: "spawnSync".to_string(),
575            command_arg_index: 0,
576            shell_by_default: false,
577            severity: Severity::High,
578            description: "Synchronously spawns process".to_string(),
579        },
580        CommandSink {
581            language: "typescript".to_string(),
582            module: Some("child_process".to_string()),
583            function: "execFile".to_string(),
584            command_arg_index: 0,
585            shell_by_default: false,
586            severity: Severity::Medium,
587            description: "Executes file directly, safer but still risky".to_string(),
588        },
589        CommandSink {
590            language: "typescript".to_string(),
591            module: Some("child_process".to_string()),
592            function: "execFileSync".to_string(),
593            command_arg_index: 0,
594            shell_by_default: false,
595            severity: Severity::Medium,
596            description: "Synchronously executes file".to_string(),
597        },
598        CommandSink {
599            language: "typescript".to_string(),
600            module: Some("child_process".to_string()),
601            function: "fork".to_string(),
602            command_arg_index: 0,
603            shell_by_default: false,
604            severity: Severity::Medium,
605            description: "Forks Node.js process".to_string(),
606        },
607        // eval - code injection
608        CommandSink {
609            language: "typescript".to_string(),
610            module: None,
611            function: "eval".to_string(),
612            command_arg_index: 0,
613            shell_by_default: false,
614            severity: Severity::Critical,
615            description: "Evaluates JavaScript code, code injection risk".to_string(),
616        },
617        CommandSink {
618            language: "typescript".to_string(),
619            module: None,
620            function: "Function".to_string(),
621            command_arg_index: 0,
622            shell_by_default: false,
623            severity: Severity::Critical,
624            description: "Creates function from string, code injection risk".to_string(),
625        },
626        // setTimeout/setInterval with string argument
627        CommandSink {
628            language: "typescript".to_string(),
629            module: None,
630            function: "setTimeout".to_string(),
631            command_arg_index: 0,
632            shell_by_default: false,
633            severity: Severity::High,
634            description: "Can execute string as code (legacy behavior)".to_string(),
635        },
636        CommandSink {
637            language: "typescript".to_string(),
638            module: None,
639            function: "setInterval".to_string(),
640            command_arg_index: 0,
641            shell_by_default: false,
642            severity: Severity::High,
643            description: "Can execute string as code (legacy behavior)".to_string(),
644        },
645        // Bun/Deno specific
646        CommandSink {
647            language: "typescript".to_string(),
648            module: Some("Bun".to_string()),
649            function: "spawn".to_string(),
650            command_arg_index: 0,
651            shell_by_default: false,
652            severity: Severity::High,
653            description: "Bun process spawning".to_string(),
654        },
655        CommandSink {
656            language: "typescript".to_string(),
657            module: Some("Deno".to_string()),
658            function: "run".to_string(),
659            command_arg_index: 0,
660            shell_by_default: false,
661            severity: Severity::High,
662            description: "Deno command execution".to_string(),
663        },
664    ]
665}
666
667fn rust_sinks() -> Vec<CommandSink> {
668    vec![
669        // std::process::Command
670        CommandSink {
671            language: "rust".to_string(),
672            module: Some("std::process".to_string()),
673            function: "Command::new".to_string(),
674            command_arg_index: 0,
675            shell_by_default: false,
676            severity: Severity::High,
677            description: "Creates command, dangerous if user controls program path".to_string(),
678        },
679        // Command builder methods that take user input
680        CommandSink {
681            language: "rust".to_string(),
682            module: Some("std::process".to_string()),
683            function: "arg".to_string(),
684            command_arg_index: 0,
685            shell_by_default: false,
686            severity: Severity::Medium,
687            description: "Adds argument to command, argument injection risk".to_string(),
688        },
689        CommandSink {
690            language: "rust".to_string(),
691            module: Some("std::process".to_string()),
692            function: "args".to_string(),
693            command_arg_index: 0,
694            shell_by_default: false,
695            severity: Severity::Medium,
696            description: "Adds arguments to command".to_string(),
697        },
698        // Shell execution via sh -c
699        CommandSink {
700            language: "rust".to_string(),
701            module: None,
702            function: "shell".to_string(),
703            command_arg_index: 0,
704            shell_by_default: true,
705            severity: Severity::Critical,
706            description: "Shell command execution pattern".to_string(),
707        },
708        // tokio::process
709        CommandSink {
710            language: "rust".to_string(),
711            module: Some("tokio::process".to_string()),
712            function: "Command::new".to_string(),
713            command_arg_index: 0,
714            shell_by_default: false,
715            severity: Severity::High,
716            description: "Async command creation".to_string(),
717        },
718    ]
719}
720
721fn go_sinks() -> Vec<CommandSink> {
722    vec![
723        // os/exec package
724        CommandSink {
725            language: "go".to_string(),
726            module: Some("os/exec".to_string()),
727            function: "Command".to_string(),
728            command_arg_index: 0,
729            shell_by_default: false,
730            severity: Severity::High,
731            description: "Creates command, dangerous if user controls program".to_string(),
732        },
733        CommandSink {
734            language: "go".to_string(),
735            module: Some("exec".to_string()),
736            function: "Command".to_string(),
737            command_arg_index: 0,
738            shell_by_default: false,
739            severity: Severity::High,
740            description: "Creates command (short import)".to_string(),
741        },
742        CommandSink {
743            language: "go".to_string(),
744            module: Some("os/exec".to_string()),
745            function: "CommandContext".to_string(),
746            command_arg_index: 1,
747            shell_by_default: false,
748            severity: Severity::High,
749            description: "Creates command with context".to_string(),
750        },
751        // syscall package
752        CommandSink {
753            language: "go".to_string(),
754            module: Some("syscall".to_string()),
755            function: "Exec".to_string(),
756            command_arg_index: 0,
757            shell_by_default: false,
758            severity: Severity::Critical,
759            description: "Low-level exec syscall".to_string(),
760        },
761        CommandSink {
762            language: "go".to_string(),
763            module: Some("syscall".to_string()),
764            function: "ForkExec".to_string(),
765            command_arg_index: 0,
766            shell_by_default: false,
767            severity: Severity::Critical,
768            description: "Fork and exec syscall".to_string(),
769        },
770        CommandSink {
771            language: "go".to_string(),
772            module: Some("syscall".to_string()),
773            function: "StartProcess".to_string(),
774            command_arg_index: 0,
775            shell_by_default: false,
776            severity: Severity::Critical,
777            description: "Starts new process".to_string(),
778        },
779    ]
780}
781
782fn c_sinks() -> Vec<CommandSink> {
783    vec![
784        // Standard library
785        CommandSink {
786            language: "c".to_string(),
787            module: None,
788            function: "system".to_string(),
789            command_arg_index: 0,
790            shell_by_default: true,
791            severity: Severity::Critical,
792            description: "Executes command in shell".to_string(),
793        },
794        CommandSink {
795            language: "c".to_string(),
796            module: None,
797            function: "popen".to_string(),
798            command_arg_index: 0,
799            shell_by_default: true,
800            severity: Severity::Critical,
801            description: "Opens pipe to shell command".to_string(),
802        },
803        // exec family
804        CommandSink {
805            language: "c".to_string(),
806            module: None,
807            function: "execl".to_string(),
808            command_arg_index: 0,
809            shell_by_default: false,
810            severity: Severity::High,
811            description: "Replaces process with new program".to_string(),
812        },
813        CommandSink {
814            language: "c".to_string(),
815            module: None,
816            function: "execle".to_string(),
817            command_arg_index: 0,
818            shell_by_default: false,
819            severity: Severity::High,
820            description: "Replaces process with environment".to_string(),
821        },
822        CommandSink {
823            language: "c".to_string(),
824            module: None,
825            function: "execlp".to_string(),
826            command_arg_index: 0,
827            shell_by_default: false,
828            severity: Severity::High,
829            description: "Replaces process using PATH".to_string(),
830        },
831        CommandSink {
832            language: "c".to_string(),
833            module: None,
834            function: "execv".to_string(),
835            command_arg_index: 0,
836            shell_by_default: false,
837            severity: Severity::High,
838            description: "Replaces process with vector args".to_string(),
839        },
840        CommandSink {
841            language: "c".to_string(),
842            module: None,
843            function: "execve".to_string(),
844            command_arg_index: 0,
845            shell_by_default: false,
846            severity: Severity::High,
847            description: "Replaces process with vector and env".to_string(),
848        },
849        CommandSink {
850            language: "c".to_string(),
851            module: None,
852            function: "execvp".to_string(),
853            command_arg_index: 0,
854            shell_by_default: false,
855            severity: Severity::High,
856            description: "Replaces process using PATH".to_string(),
857        },
858        CommandSink {
859            language: "c".to_string(),
860            module: None,
861            function: "execvpe".to_string(),
862            command_arg_index: 0,
863            shell_by_default: false,
864            severity: Severity::High,
865            description: "Replaces process using PATH with env".to_string(),
866        },
867        // fork/spawn
868        CommandSink {
869            language: "c".to_string(),
870            module: None,
871            function: "posix_spawn".to_string(),
872            command_arg_index: 1,
873            shell_by_default: false,
874            severity: Severity::High,
875            description: "POSIX spawn interface".to_string(),
876        },
877        CommandSink {
878            language: "c".to_string(),
879            module: None,
880            function: "posix_spawnp".to_string(),
881            command_arg_index: 1,
882            shell_by_default: false,
883            severity: Severity::High,
884            description: "POSIX spawn with PATH search".to_string(),
885        },
886    ]
887}
888
889fn java_sinks() -> Vec<CommandSink> {
890    vec![
891        // Runtime.exec
892        CommandSink {
893            language: "java".to_string(),
894            module: Some("java.lang.Runtime".to_string()),
895            function: "exec".to_string(),
896            command_arg_index: 0,
897            shell_by_default: false,
898            severity: Severity::Critical,
899            description: "Executes system command".to_string(),
900        },
901        CommandSink {
902            language: "java".to_string(),
903            module: Some("Runtime".to_string()),
904            function: "exec".to_string(),
905            command_arg_index: 0,
906            shell_by_default: false,
907            severity: Severity::Critical,
908            description: "Executes system command (short form)".to_string(),
909        },
910        // ProcessBuilder
911        CommandSink {
912            language: "java".to_string(),
913            module: Some("java.lang.ProcessBuilder".to_string()),
914            function: "command".to_string(),
915            command_arg_index: 0,
916            shell_by_default: false,
917            severity: Severity::High,
918            description: "Sets process command".to_string(),
919        },
920        CommandSink {
921            language: "java".to_string(),
922            module: Some("ProcessBuilder".to_string()),
923            function: "command".to_string(),
924            command_arg_index: 0,
925            shell_by_default: false,
926            severity: Severity::High,
927            description: "Sets process command (short form)".to_string(),
928        },
929        // Constructor with command
930        CommandSink {
931            language: "java".to_string(),
932            module: None,
933            function: "ProcessBuilder".to_string(),
934            command_arg_index: 0,
935            shell_by_default: false,
936            severity: Severity::High,
937            description: "Creates ProcessBuilder with command".to_string(),
938        },
939        // Script engines
940        CommandSink {
941            language: "java".to_string(),
942            module: Some("javax.script.ScriptEngine".to_string()),
943            function: "eval".to_string(),
944            command_arg_index: 0,
945            shell_by_default: false,
946            severity: Severity::Critical,
947            description: "Evaluates script code".to_string(),
948        },
949    ]
950}
951
952// =============================================================================
953// Taint Source Detection
954// =============================================================================
955
956/// Get tree-sitter query for detecting taint sources in a language.
957fn get_taint_source_query(language: &str) -> Option<&'static str> {
958    match language {
959        "python" => Some(PYTHON_TAINT_SOURCES_QUERY),
960        "typescript" | "javascript" => Some(TYPESCRIPT_TAINT_SOURCES_QUERY),
961        "go" => Some(GO_TAINT_SOURCES_QUERY),
962        "rust" => Some(RUST_TAINT_SOURCES_QUERY),
963        "c" | "cpp" => Some(C_TAINT_SOURCES_QUERY),
964        "java" => Some(JAVA_TAINT_SOURCES_QUERY),
965        _ => None,
966    }
967}
968
969/// Python taint sources query.
970const PYTHON_TAINT_SOURCES_QUERY: &str = r#"
971; HTTP request parameters (Flask, Django, FastAPI)
972(attribute object: (identifier) @obj attribute: (identifier) @attr
973  (#any-of? @obj "request" "req")
974  (#any-of? @attr "args" "form" "data" "json" "values" "files" "headers" "cookies" "get_json" "params" "query_params" "body")) @source
975
976; request.GET/POST (Django)
977(subscript value: (attribute object: (identifier) @obj attribute: (identifier) @attr)
978  (#eq? @obj "request")
979  (#any-of? @attr "GET" "POST" "FILES" "COOKIES")) @source
980
981; input() builtin
982(call function: (identifier) @func (#eq? @func "input")) @source
983
984; Environment variables
985(subscript value: (attribute object: (identifier) @obj attribute: (identifier) @attr)
986  (#eq? @obj "os")
987  (#eq? @attr "environ")) @source
988(call function: (attribute object: (identifier) @obj attribute: (identifier) @attr)
989  (#eq? @obj "os")
990  (#any-of? @attr "getenv" "environ")) @source
991
992; File read operations
993(call function: (attribute attribute: (identifier) @method)
994  (#any-of? @method "read" "readline" "readlines")) @source
995
996; sys.argv
997(subscript value: (attribute object: (identifier) @obj attribute: (identifier) @attr)
998  (#eq? @obj "sys")
999  (#eq? @attr "argv")) @source
1000(attribute object: (identifier) @obj attribute: (identifier) @attr
1001  (#eq? @obj "sys")
1002  (#eq? @attr "argv")) @source
1003
1004; stdin
1005(attribute object: (identifier) @obj attribute: (identifier) @attr
1006  (#eq? @obj "sys")
1007  (#eq? @attr "stdin")) @source
1008"#;
1009
1010/// TypeScript/JavaScript taint sources query.
1011const TYPESCRIPT_TAINT_SOURCES_QUERY: &str = r#"
1012; Express req.body, req.query, req.params
1013(member_expression object: (identifier) @obj property: (property_identifier) @prop
1014  (#eq? @obj "req")
1015  (#any-of? @prop "body" "query" "params" "headers" "cookies")) @source
1016
1017; request object properties
1018(member_expression object: (identifier) @obj property: (property_identifier) @prop
1019  (#eq? @obj "request")
1020  (#any-of? @prop "body" "query" "params" "headers")) @source
1021
1022; process.argv
1023(member_expression object: (member_expression object: (identifier) @obj property: (property_identifier) @prop)
1024  (#eq? @obj "process")
1025  (#eq? @prop "argv")) @source
1026(member_expression object: (identifier) @obj property: (property_identifier) @prop
1027  (#eq? @obj "process")
1028  (#eq? @prop "argv")) @source
1029
1030; process.env
1031(member_expression object: (member_expression object: (identifier) @obj property: (property_identifier) @prop)
1032  (#eq? @obj "process")
1033  (#eq? @prop "env")) @source
1034
1035; readline input
1036(call_expression function: (member_expression property: (property_identifier) @method)
1037  (#any-of? @method "question" "prompt")) @source
1038
1039; DOM input
1040(call_expression function: (member_expression property: (property_identifier) @method)
1041  (#any-of? @method "getElementById" "querySelector" "querySelectorAll")) @source
1042
1043; URL search params
1044(new_expression constructor: (identifier) @ctor
1045  (#eq? @ctor "URLSearchParams")) @source
1046"#;
1047
1048/// Go taint sources query.
1049const GO_TAINT_SOURCES_QUERY: &str = r#"
1050; HTTP request
1051(selector_expression operand: (identifier) @obj field: (field_identifier) @field
1052  (#any-of? @obj "r" "req" "request")
1053  (#any-of? @field "Body" "URL" "Form" "PostForm" "Header")) @source
1054
1055; URL query
1056(call_expression function: (selector_expression field: (field_identifier) @method)
1057  (#any-of? @method "Query" "FormValue" "PostFormValue")) @source
1058
1059; os.Args
1060(selector_expression operand: (identifier) @pkg field: (field_identifier) @field
1061  (#eq? @pkg "os")
1062  (#eq? @field "Args")) @source
1063
1064; Environment
1065(call_expression function: (selector_expression operand: (identifier) @pkg field: (field_identifier) @method)
1066  (#eq? @pkg "os")
1067  (#any-of? @method "Getenv" "LookupEnv" "Environ")) @source
1068
1069; flag package
1070(call_expression function: (selector_expression operand: (identifier) @pkg)
1071  (#eq? @pkg "flag")) @source
1072
1073; stdin
1074(selector_expression operand: (identifier) @pkg field: (field_identifier) @field
1075  (#eq? @pkg "os")
1076  (#eq? @field "Stdin")) @source
1077"#;
1078
1079/// Rust taint sources query.
1080/// Note: tree-sitter-rust doesn't have method_call_expression, so HTTP framework
1081/// detection is limited.
1082const RUST_TAINT_SOURCES_QUERY: &str = r#"
1083; std::env::args
1084(call_expression function: (scoped_identifier) @func
1085  (#match? @func "std::env::args")) @source
1086(call_expression function: (scoped_identifier) @func
1087  (#match? @func "env::args")) @source
1088
1089; std::env::var
1090(call_expression function: (scoped_identifier) @func
1091  (#match? @func "std::env::var")) @source
1092(call_expression function: (scoped_identifier) @func
1093  (#match? @func "env::var")) @source
1094
1095; stdin read
1096(call_expression function: (scoped_identifier) @func
1097  (#match? @func "stdin")) @source
1098"#;
1099
1100/// C taint sources query.
1101const C_TAINT_SOURCES_QUERY: &str = r#"
1102; argv parameter
1103(parameter_declaration declarator: (pointer_declarator declarator: (array_declarator declarator: (identifier) @name))
1104  (#eq? @name "argv")) @source
1105(parameter_declaration declarator: (pointer_declarator declarator: (pointer_declarator declarator: (identifier) @name))
1106  (#eq? @name "argv")) @source
1107
1108; getenv
1109(call_expression function: (identifier) @func
1110  (#eq? @func "getenv")) @source
1111
1112; stdin read
1113(call_expression function: (identifier) @func
1114  (#any-of? @func "fgets" "gets" "scanf" "fscanf" "getchar" "fgetc" "getc" "fread")) @source
1115
1116; Environment via environ
1117(identifier) @source
1118  (#eq? @source "environ")
1119"#;
1120
1121/// Java taint sources query.
1122const JAVA_TAINT_SOURCES_QUERY: &str = r#"
1123; HTTP servlet request
1124(method_invocation object: (identifier) @obj name: (identifier) @method
1125  (#any-of? @obj "request" "req" "httpRequest")
1126  (#any-of? @method "getParameter" "getParameterValues" "getParameterMap" "getHeader" "getHeaders" "getCookies" "getInputStream" "getReader")) @source
1127
1128; Spring @RequestParam, @RequestBody (harder to detect, but method calls)
1129(method_invocation name: (identifier) @method
1130  (#any-of? @method "getParameter" "getBody" "getHeaders")) @source
1131
1132; System.getenv
1133(method_invocation object: (identifier) @obj name: (identifier) @method
1134  (#eq? @obj "System")
1135  (#any-of? @method "getenv" "getProperty")) @source
1136
1137; args[] in main
1138(array_access array: (identifier) @arr
1139  (#eq? @arr "args")) @source
1140
1141; Scanner input
1142(method_invocation object: (identifier) @obj name: (identifier) @method
1143  (#any-of? @method "nextLine" "next" "nextInt" "nextDouble")) @source
1144
1145; BufferedReader
1146(method_invocation name: (identifier) @method
1147  (#eq? @method "readLine")) @source
1148"#;
1149
1150// =============================================================================
1151// Sink Detection
1152// =============================================================================
1153
1154/// Get tree-sitter query for detecting command sinks in a language.
1155fn get_sink_query(language: &str) -> Option<&'static str> {
1156    match language {
1157        "python" => Some(PYTHON_SINK_QUERY),
1158        "typescript" | "javascript" => Some(TYPESCRIPT_SINK_QUERY),
1159        "go" => Some(GO_SINK_QUERY),
1160        "rust" => Some(RUST_SINK_QUERY),
1161        "c" | "cpp" => Some(C_SINK_QUERY),
1162        "java" => Some(JAVA_SINK_QUERY),
1163        _ => None,
1164    }
1165}
1166
1167/// Python command sink detection query.
1168const PYTHON_SINK_QUERY: &str = r#"
1169; os.system, os.popen, etc.
1170(call function: (attribute object: (identifier) @module attribute: (identifier) @func)
1171  (#eq? @module "os")
1172  (#any-of? @func "system" "popen" "spawn" "spawnl" "spawnle" "spawnlp" "spawnlpe" "spawnv" "spawnve" "spawnvp" "spawnvpe" "execl" "execle" "execlp" "execlpe" "execv" "execve" "execvp" "execvpe")
1173  arguments: (argument_list) @args) @sink
1174
1175; subprocess module calls
1176(call function: (attribute object: (identifier) @module attribute: (identifier) @func)
1177  (#eq? @module "subprocess")
1178  (#any-of? @func "call" "run" "Popen" "check_call" "check_output" "getoutput" "getstatusoutput")
1179  arguments: (argument_list) @args) @sink
1180
1181; commands module (deprecated)
1182(call function: (attribute object: (identifier) @module attribute: (identifier) @func)
1183  (#eq? @module "commands")
1184  (#any-of? @func "getoutput" "getstatusoutput")
1185  arguments: (argument_list) @args) @sink
1186
1187; eval/exec builtins
1188(call function: (identifier) @func
1189  (#any-of? @func "eval" "exec" "compile")
1190  arguments: (argument_list) @args) @sink
1191
1192; pty.spawn
1193(call function: (attribute object: (identifier) @module attribute: (identifier) @func)
1194  (#eq? @module "pty")
1195  (#eq? @func "spawn")
1196  arguments: (argument_list) @args) @sink
1197"#;
1198
1199/// TypeScript/JavaScript command sink detection query.
1200const TYPESCRIPT_SINK_QUERY: &str = r#"
1201; child_process.exec, spawn, etc.
1202(call_expression function: (member_expression object: (identifier) @module property: (property_identifier) @func)
1203  (#any-of? @module "child_process" "cp")
1204  (#any-of? @func "exec" "execSync" "spawn" "spawnSync" "execFile" "execFileSync" "fork")
1205  arguments: (arguments) @args) @sink
1206
1207; require('child_process').exec pattern
1208(call_expression function: (member_expression object: (call_expression function: (identifier) @req arguments: (arguments (string) @mod))
1209    property: (property_identifier) @func)
1210  (#eq? @req "require")
1211  (#match? @mod "child_process")
1212  (#any-of? @func "exec" "execSync" "spawn" "spawnSync" "execFile" "execFileSync")
1213  arguments: (arguments) @args) @sink
1214
1215; eval
1216(call_expression function: (identifier) @func
1217  (#eq? @func "eval")
1218  arguments: (arguments) @args) @sink
1219
1220; Function constructor
1221(new_expression constructor: (identifier) @func
1222  (#eq? @func "Function")
1223  arguments: (arguments) @args) @sink
1224
1225; setTimeout/setInterval with string
1226(call_expression function: (identifier) @func
1227  (#any-of? @func "setTimeout" "setInterval")
1228  arguments: (arguments (string) @str_arg)) @sink
1229
1230; Bun.spawn, Deno.run
1231(call_expression function: (member_expression object: (identifier) @obj property: (property_identifier) @func)
1232  (#any-of? @obj "Bun" "Deno")
1233  (#any-of? @func "spawn" "run")
1234  arguments: (arguments) @args) @sink
1235"#;
1236
1237/// Go command sink detection query.
1238const GO_SINK_QUERY: &str = r#"
1239; exec.Command
1240(call_expression function: (selector_expression operand: (identifier) @pkg field: (field_identifier) @func)
1241  (#any-of? @pkg "exec" "os/exec")
1242  (#any-of? @func "Command" "CommandContext")
1243  arguments: (argument_list) @args) @sink
1244
1245; syscall.Exec, ForkExec
1246(call_expression function: (selector_expression operand: (identifier) @pkg field: (field_identifier) @func)
1247  (#eq? @pkg "syscall")
1248  (#any-of? @func "Exec" "ForkExec" "StartProcess")
1249  arguments: (argument_list) @args) @sink
1250
1251; os.StartProcess
1252(call_expression function: (selector_expression operand: (identifier) @pkg field: (field_identifier) @func)
1253  (#eq? @pkg "os")
1254  (#eq? @func "StartProcess")
1255  arguments: (argument_list) @args) @sink
1256"#;
1257
1258/// Rust command sink detection query.
1259/// Note: In tree-sitter-rust, method calls use `call_expression` not a separate node type.
1260const RUST_SINK_QUERY: &str = r#"
1261; Command::new - scoped path
1262(call_expression function: (scoped_identifier) @func
1263  (#match? @func "Command::new")
1264  arguments: (arguments) @args) @sink
1265
1266; std::process::Command::new - fully qualified
1267(call_expression function: (scoped_identifier) @func
1268  (#match? @func "std::process::Command::new")
1269  arguments: (arguments) @args) @sink
1270
1271; tokio::process::Command::new - async version
1272(call_expression function: (scoped_identifier) @func
1273  (#match? @func "tokio::process::Command::new")
1274  arguments: (arguments) @args) @sink
1275
1276; Generic new() call that might be Command
1277(call_expression function: (field_expression value: (identifier) @obj field: (field_identifier) @method)
1278  (#eq? @method "new")
1279  arguments: (arguments) @args) @sink
1280"#;
1281
1282/// C command sink detection query.
1283const C_SINK_QUERY: &str = r#"
1284; system(), popen()
1285(call_expression function: (identifier) @func
1286  (#any-of? @func "system" "popen")
1287  arguments: (argument_list) @args) @sink
1288
1289; exec family
1290(call_expression function: (identifier) @func
1291  (#any-of? @func "execl" "execle" "execlp" "execv" "execve" "execvp" "execvpe")
1292  arguments: (argument_list) @args) @sink
1293
1294; posix_spawn
1295(call_expression function: (identifier) @func
1296  (#any-of? @func "posix_spawn" "posix_spawnp")
1297  arguments: (argument_list) @args) @sink
1298"#;
1299
1300/// Java command sink detection query.
1301const JAVA_SINK_QUERY: &str = r#"
1302; Runtime.exec
1303(method_invocation object: (method_invocation object: (identifier) @cls name: (identifier) @get)
1304  name: (identifier) @method
1305  (#eq? @cls "Runtime")
1306  (#eq? @get "getRuntime")
1307  (#eq? @method "exec")
1308  arguments: (argument_list) @args) @sink
1309
1310; Direct Runtime.getRuntime().exec
1311(method_invocation name: (identifier) @method
1312  (#eq? @method "exec")
1313  arguments: (argument_list) @args) @sink
1314
1315; ProcessBuilder constructor
1316(object_creation_expression type: (type_identifier) @type
1317  (#eq? @type "ProcessBuilder")
1318  arguments: (argument_list) @args) @sink
1319
1320; ProcessBuilder.command
1321(method_invocation name: (identifier) @method
1322  (#eq? @method "command")
1323  arguments: (argument_list) @args) @sink
1324
1325; ScriptEngine.eval
1326(method_invocation object: (identifier) @obj name: (identifier) @method
1327  (#eq? @method "eval")
1328  arguments: (argument_list) @args) @sink
1329"#;
1330
1331// =============================================================================
1332// Scanning Implementation
1333// =============================================================================
1334
1335/// Scan a directory for command injection vulnerabilities.
1336///
1337/// # Arguments
1338///
1339/// * `path` - Directory to scan
1340/// * `language` - Optional language filter (scans all supported languages if None)
1341///
1342/// # Returns
1343///
1344/// Vector of command injection findings.
1345pub fn scan_command_injection(path: &Path, language: Option<&str>) -> Result<Vec<CommandInjectionFinding>> {
1346    let path_str = path.to_str().ok_or_else(|| {
1347        BrrrError::InvalidArgument("Invalid path encoding".to_string())
1348    })?;
1349
1350    let scanner = ProjectScanner::new(path_str)?;
1351    let config = match language {
1352        Some(lang) => ScanConfig::for_language(lang),
1353        None => ScanConfig::default(),
1354    };
1355
1356    let scan_result = scanner.scan_with_config(&config)?;
1357    let files = scan_result.files;
1358
1359    // Process files in parallel
1360    let findings: Vec<CommandInjectionFinding> = files
1361        .par_iter()
1362        .filter_map(|file| {
1363            scan_file_command_injection(file, language).ok()
1364        })
1365        .flatten()
1366        .collect();
1367
1368    Ok(findings)
1369}
1370
1371/// Scan a single file for command injection vulnerabilities.
1372///
1373/// # Arguments
1374///
1375/// * `file` - Path to the file to scan
1376/// * `language` - Optional language override (auto-detected if None)
1377///
1378/// # Returns
1379///
1380/// Vector of command injection findings in this file.
1381pub fn scan_file_command_injection(file: &Path, language: Option<&str>) -> Result<Vec<CommandInjectionFinding>> {
1382    let registry = LanguageRegistry::global();
1383
1384    // Detect language
1385    let lang = match language {
1386        Some(lang_name) => registry
1387            .get_by_name(lang_name)
1388            .ok_or_else(|| BrrrError::UnsupportedLanguage(lang_name.to_string()))?,
1389        None => registry
1390            .detect_language(file)
1391            .ok_or_else(|| BrrrError::UnsupportedLanguage(
1392                file.extension()
1393                    .and_then(|e| e.to_str())
1394                    .unwrap_or("unknown")
1395                    .to_string(),
1396            ))?,
1397    };
1398
1399    let lang_name = lang.name();
1400
1401    // Get queries for this language
1402    let sink_query_str = get_sink_query(lang_name)
1403        .ok_or_else(|| BrrrError::UnsupportedLanguage(format!("{} (no sink query)", lang_name)))?;
1404
1405    let taint_query_str = get_taint_source_query(lang_name);
1406
1407    // Parse the file
1408    let source = std::fs::read(file).map_err(|e| BrrrError::io_with_path(e, file))?;
1409    let mut parser = lang.parser_for_path(file)?;
1410    let tree = parser.parse(&source, None).ok_or_else(|| BrrrError::Parse {
1411        file: file.display().to_string(),
1412        message: "Failed to parse file".to_string(),
1413    })?;
1414
1415    let ts_lang = tree.language();
1416    let file_path = file.display().to_string();
1417
1418    // Find sinks
1419    let sinks = find_sinks(&tree, &source, &ts_lang, sink_query_str, lang_name, &file_path)?;
1420
1421    // Find taint sources if query is available
1422    let taint_sources = if let Some(taint_query) = taint_query_str {
1423        find_taint_sources(&tree, &source, &ts_lang, taint_query, lang_name, &file_path)?
1424    } else {
1425        HashMap::new()
1426    };
1427
1428    // Analyze each sink for potential injection
1429    let mut findings = Vec::new();
1430    for (sink_loc, sink_info) in sinks {
1431        let finding = analyze_sink(
1432            &sink_info,
1433            &sink_loc,
1434            &taint_sources,
1435            &source,
1436            &tree,
1437            lang_name,
1438            &file_path,
1439        );
1440        if let Some(f) = finding {
1441            findings.push(f);
1442        }
1443    }
1444
1445    Ok(findings)
1446}
1447
1448/// Information about a detected sink.
1449#[derive(Debug)]
1450struct SinkInfo {
1451    function_name: String,
1452    arguments_node: Option<tree_sitter::Range>,
1453    first_arg_text: Option<String>,
1454    has_shell_true: bool,
1455}
1456
1457/// Find all command execution sinks in the parsed tree.
1458fn find_sinks(
1459    tree: &Tree,
1460    source: &[u8],
1461    ts_lang: &tree_sitter::Language,
1462    query_str: &str,
1463    lang_name: &str,
1464    file_path: &str,
1465) -> Result<HashMap<SourceLocation, SinkInfo>> {
1466    let query = Query::new(ts_lang, query_str)
1467        .map_err(|e| BrrrError::TreeSitter(format_query_error(lang_name, "sink", query_str, &e)))?;
1468
1469    let mut cursor = QueryCursor::new();
1470    let mut matches = cursor.matches(&query, tree.root_node(), source);
1471
1472    // Get capture indices
1473    let sink_idx = query.capture_index_for_name("sink");
1474    let func_idx = query.capture_index_for_name("func");
1475    let args_idx = query.capture_index_for_name("args");
1476
1477    let mut sinks = HashMap::new();
1478
1479    while let Some(match_) = matches.next() {
1480        let sink_node = sink_idx
1481            .and_then(|idx| match_.captures.iter().find(|c| c.index == idx))
1482            .map(|c| c.node);
1483
1484        let func_node = func_idx
1485            .and_then(|idx| match_.captures.iter().find(|c| c.index == idx))
1486            .map(|c| c.node);
1487
1488        let args_node = args_idx
1489            .and_then(|idx| match_.captures.iter().find(|c| c.index == idx))
1490            .map(|c| c.node);
1491
1492        if let Some(sink_node) = sink_node {
1493            let location = SourceLocation {
1494                file: file_path.to_string(),
1495                line: sink_node.start_position().row + 1,
1496                column: sink_node.start_position().column + 1,
1497                end_line: sink_node.end_position().row + 1,
1498                end_column: sink_node.end_position().column + 1,
1499            };
1500
1501            let function_name = func_node
1502                .map(|n| node_text(n, source).to_string())
1503                .unwrap_or_else(|| "unknown".to_string());
1504
1505            let first_arg_text = args_node.and_then(|args| {
1506                extract_first_argument(args, source)
1507            });
1508
1509            let has_shell_true = args_node
1510                .map(|args| check_shell_true(args, source, lang_name))
1511                .unwrap_or(false);
1512
1513            sinks.insert(location, SinkInfo {
1514                function_name,
1515                arguments_node: args_node.map(|n| n.range()),
1516                first_arg_text,
1517                has_shell_true,
1518            });
1519        }
1520    }
1521
1522    Ok(sinks)
1523}
1524
1525/// Find all taint sources in the parsed tree.
1526fn find_taint_sources(
1527    tree: &Tree,
1528    source: &[u8],
1529    ts_lang: &tree_sitter::Language,
1530    query_str: &str,
1531    lang_name: &str,
1532    file_path: &str,
1533) -> Result<HashMap<String, Vec<TaintSource>>> {
1534    let query = Query::new(ts_lang, query_str)
1535        .map_err(|e| BrrrError::TreeSitter(format_query_error(lang_name, "taint_source", query_str, &e)))?;
1536
1537    let mut cursor = QueryCursor::new();
1538    let mut matches = cursor.matches(&query, tree.root_node(), source);
1539
1540    let source_idx = query.capture_index_for_name("source");
1541
1542    let mut sources: HashMap<String, Vec<TaintSource>> = HashMap::new();
1543
1544    while let Some(match_) = matches.next() {
1545        if let Some(idx) = source_idx {
1546            if let Some(capture) = match_.captures.iter().find(|c| c.index == idx) {
1547                let node = capture.node;
1548                let text = node_text(node, source);
1549
1550                // Try to extract variable name from assignment context
1551                let variable = extract_assigned_variable(node, source)
1552                    .unwrap_or_else(|| text.to_string());
1553
1554                let kind = classify_taint_source(text, lang_name);
1555
1556                let location = SourceLocation {
1557                    file: file_path.to_string(),
1558                    line: node.start_position().row + 1,
1559                    column: node.start_position().column + 1,
1560                    end_line: node.end_position().row + 1,
1561                    end_column: node.end_position().column + 1,
1562                };
1563
1564                let taint = TaintSource {
1565                    kind,
1566                    variable: variable.clone(),
1567                    location,
1568                    description: format!("Taint from {}", text),
1569                };
1570
1571                sources.entry(variable).or_default().push(taint);
1572            }
1573        }
1574    }
1575
1576    Ok(sources)
1577}
1578
1579/// Classify the type of taint source based on the expression.
1580fn classify_taint_source(text: &str, _lang: &str) -> TaintSourceKind {
1581    let lower = text.to_lowercase();
1582
1583    if lower.contains("request") || lower.contains("req.") {
1584        if lower.contains("body") || lower.contains("json") || lower.contains("form") {
1585            TaintSourceKind::FormInput
1586        } else {
1587            TaintSourceKind::HttpRequest
1588        }
1589    } else if lower.contains("stdin") || lower.contains("input") || lower.contains("readline") {
1590        TaintSourceKind::StdIn
1591    } else if lower.contains("getenv") || lower.contains("environ") || lower.contains("env.") {
1592        TaintSourceKind::EnvVar
1593    } else if lower.contains("argv") || lower.contains("args") {
1594        TaintSourceKind::CmdLineArg
1595    } else if lower.contains("read") || lower.contains("file") {
1596        TaintSourceKind::FileRead
1597    } else {
1598        TaintSourceKind::Unknown
1599    }
1600}
1601
1602/// Extract the variable being assigned if this node is part of an assignment.
1603fn extract_assigned_variable(node: Node, source: &[u8]) -> Option<String> {
1604    // Walk up to find assignment
1605    let mut current = node;
1606    while let Some(parent) = current.parent() {
1607        match parent.kind() {
1608            "assignment" | "assignment_statement" | "variable_declaration" | "lexical_declaration" => {
1609                // Get the left side of assignment
1610                let mut cursor = parent.walk();
1611                for child in parent.children(&mut cursor) {
1612                    if child.kind() == "identifier" || child.kind() == "pattern" {
1613                        return Some(node_text(child, source).to_string());
1614                    }
1615                    // For Python: pattern_list, tuple_pattern, etc.
1616                    if child.end_byte() < node.start_byte() {
1617                        // This child is before the node, likely the target
1618                        let text = node_text(child, source);
1619                        if !text.contains('(') && !text.contains('[') {
1620                            return Some(text.to_string());
1621                        }
1622                    }
1623                }
1624            }
1625            _ => {}
1626        }
1627        current = parent;
1628    }
1629    None
1630}
1631
1632/// Analyze a sink to determine if it's vulnerable.
1633fn analyze_sink(
1634    sink: &SinkInfo,
1635    location: &SourceLocation,
1636    taint_sources: &HashMap<String, Vec<TaintSource>>,
1637    source: &[u8],
1638    tree: &Tree,
1639    lang_name: &str,
1640    file_path: &str,
1641) -> Option<CommandInjectionFinding> {
1642    let known_sinks = get_command_sinks(lang_name);
1643    let sink_def = known_sinks.iter().find(|s| s.function == sink.function_name)?;
1644
1645    // Determine injection kind
1646    let kind = if sink.function_name == "eval" || sink.function_name == "exec" || sink.function_name == "compile" {
1647        InjectionKind::CodeInjection
1648    } else if sink_def.shell_by_default || sink.has_shell_true {
1649        InjectionKind::CommandInjection
1650    } else {
1651        InjectionKind::ArgumentInjection
1652    };
1653
1654    // Analyze the first argument for taint
1655    let (tainted_input, confidence, taint_chain) = if let Some(ref arg_text) = sink.first_arg_text {
1656        analyze_argument_taint(arg_text, taint_sources, tree, source, file_path)
1657    } else {
1658        ("unknown".to_string(), Confidence::Low, vec![])
1659    };
1660
1661    // Adjust severity based on context
1662    // Code injection (eval/exec/compile) is ALWAYS critical - even pattern matches are dangerous
1663    let severity = if kind == InjectionKind::CodeInjection {
1664        // eval/exec/compile are inherently critical - arbitrary code execution
1665        Severity::Critical
1666    } else if sink_def.shell_by_default || sink.has_shell_true {
1667        // Shell execution with user input is always critical
1668        if confidence >= Confidence::Medium {
1669            Severity::Critical
1670        } else {
1671            sink_def.severity
1672        }
1673    } else if confidence == Confidence::High {
1674        // Direct taint to non-shell sink is high
1675        Severity::High
1676    } else if confidence == Confidence::Medium {
1677        Severity::Medium
1678    } else {
1679        // Pattern match only - use sink's defined severity as minimum
1680        sink_def.severity.min(Severity::Low)
1681    };
1682
1683    // Generate code snippet
1684    let code_snippet = extract_code_snippet(source, location);
1685
1686    // Generate remediation advice
1687    let remediation = generate_remediation(lang_name, &sink.function_name, kind);
1688
1689    Some(CommandInjectionFinding {
1690        location: location.clone(),
1691        severity,
1692        sink_function: sink.function_name.clone(),
1693        tainted_input,
1694        confidence,
1695        kind,
1696        taint_chain,
1697        code_snippet,
1698        remediation,
1699    })
1700}
1701
1702/// Analyze an argument for taint propagation.
1703fn analyze_argument_taint(
1704    arg_text: &str,
1705    taint_sources: &HashMap<String, Vec<TaintSource>>,
1706    _tree: &Tree,
1707    _source: &[u8],
1708    _file_path: &str,
1709) -> (String, Confidence, Vec<TaintSource>) {
1710    // Check for direct taint (variable matches a taint source)
1711    for (var_name, sources) in taint_sources {
1712        if arg_text.contains(var_name) {
1713            return (
1714                var_name.clone(),
1715                Confidence::High,
1716                sources.clone(),
1717            );
1718        }
1719    }
1720
1721    // Check for suspicious patterns even without direct taint tracking
1722    let suspicious_patterns = [
1723        "request", "req", "params", "query", "body", "input",
1724        "argv", "args", "env", "getenv", "user", "data",
1725        "stdin", "file", "read", "form",
1726    ];
1727
1728    let lower = arg_text.to_lowercase();
1729    for pattern in suspicious_patterns {
1730        if lower.contains(pattern) {
1731            return (
1732                arg_text.to_string(),
1733                Confidence::Medium,
1734                vec![],
1735            );
1736        }
1737    }
1738
1739    // Check for string concatenation or interpolation with variables
1740    if arg_text.contains('+') || arg_text.contains("format") ||
1741       arg_text.contains('%') || arg_text.contains('{') ||
1742       arg_text.contains('$') || arg_text.contains('`') {
1743        return (
1744            arg_text.to_string(),
1745            Confidence::Medium,
1746            vec![],
1747        );
1748    }
1749
1750    // If it's a variable (not a literal), flag with low confidence
1751    if !arg_text.starts_with('"') && !arg_text.starts_with('\'') &&
1752       !arg_text.starts_with('[') && !arg_text.chars().next().map(|c| c.is_numeric()).unwrap_or(false) {
1753        return (
1754            arg_text.to_string(),
1755            Confidence::Low,
1756            vec![],
1757        );
1758    }
1759
1760    (arg_text.to_string(), Confidence::Low, vec![])
1761}
1762
1763/// Check if subprocess call has shell=True.
1764fn check_shell_true(args_node: Node, source: &[u8], lang: &str) -> bool {
1765    let text = node_text(args_node, source);
1766
1767    match lang {
1768        "python" => text.contains("shell=True") || text.contains("shell = True"),
1769        "typescript" | "javascript" => text.contains("shell: true") || text.contains("shell:true"),
1770        _ => false,
1771    }
1772}
1773
1774/// Extract the first argument from an argument list.
1775fn extract_first_argument(args_node: Node, source: &[u8]) -> Option<String> {
1776    let mut cursor = args_node.walk();
1777    for child in args_node.children(&mut cursor) {
1778        // Skip punctuation
1779        if child.kind() == "(" || child.kind() == ")" || child.kind() == "," {
1780            continue;
1781        }
1782        // Return first actual argument
1783        return Some(node_text(child, source).to_string());
1784    }
1785    None
1786}
1787
1788/// Extract a code snippet around the finding.
1789fn extract_code_snippet(source: &[u8], location: &SourceLocation) -> Option<String> {
1790    let source_str = std::str::from_utf8(source).ok()?;
1791    let lines: Vec<&str> = source_str.lines().collect();
1792
1793    // Get lines around the finding (1 before, finding line, 1 after)
1794    let start = location.line.saturating_sub(2);
1795    let end = (location.end_line + 1).min(lines.len());
1796
1797    let snippet: Vec<String> = lines[start..end]
1798        .iter()
1799        .enumerate()
1800        .map(|(i, line)| format!("{:4} | {}", start + i + 1, line))
1801        .collect();
1802
1803    Some(snippet.join("\n"))
1804}
1805
1806/// Generate remediation advice for the finding.
1807fn generate_remediation(lang: &str, function: &str, kind: InjectionKind) -> String {
1808    match kind {
1809        InjectionKind::CodeInjection => {
1810            "CRITICAL: Never pass user input to eval/exec/compile. Use safer alternatives:\n\
1811             - JSON parsing for data: json.loads() / JSON.parse()\n\
1812             - AST parsing for expressions: ast.literal_eval() (Python)\n\
1813             - Template engines for dynamic content\n\
1814             - If absolutely necessary, use strict whitelisting and sandboxing".to_string()
1815        }
1816        InjectionKind::CommandInjection => {
1817            match lang {
1818                "python" => format!(
1819                    "CRITICAL: {} uses shell=True or is inherently shell-based.\n\
1820                     Fix: Use subprocess with a list of arguments and shell=False:\n\
1821                     - subprocess.run(['cmd', arg1, arg2], shell=False)\n\
1822                     - Never concatenate user input into command strings\n\
1823                     - Validate/whitelist allowed commands and arguments\n\
1824                     - Use shlex.quote() if shell execution is unavoidable",
1825                    function
1826                ),
1827                "typescript" | "javascript" => format!(
1828                    "CRITICAL: {} executes commands in a shell.\n\
1829                     Fix: Use execFile or spawn without shell option:\n\
1830                     - execFile('/bin/cmd', [arg1, arg2])\n\
1831                     - spawn('cmd', [arg1, arg2]) without shell:true\n\
1832                     - Validate/whitelist allowed commands\n\
1833                     - Never concatenate user input into command strings",
1834                    function
1835                ),
1836                "go" => format!(
1837                    "CRITICAL: {} executes system commands.\n\
1838                     Fix: Use exec.Command with separate arguments:\n\
1839                     - exec.Command(\"cmd\", arg1, arg2) not exec.Command(\"sh\", \"-c\", userInput)\n\
1840                     - Validate/whitelist allowed commands and arguments\n\
1841                     - Never use string concatenation for commands",
1842                    function
1843                ),
1844                "c" | "cpp" => format!(
1845                    "CRITICAL: {} executes commands via shell.\n\
1846                     Fix: Use exec* family functions with explicit arguments:\n\
1847                     - execv() or execvp() with argument array\n\
1848                     - Never pass user input to system() or popen()\n\
1849                     - Validate/whitelist all inputs before use",
1850                    function
1851                ),
1852                "java" => format!(
1853                    "CRITICAL: {} executes system commands.\n\
1854                     Fix: Use ProcessBuilder with argument list:\n\
1855                     - new ProcessBuilder(\"cmd\", arg1, arg2)\n\
1856                     - Avoid Runtime.exec(string) with concatenated commands\n\
1857                     - Validate/whitelist allowed commands and arguments",
1858                    function
1859                ),
1860                _ => "Use parameterized command execution without shell interpretation".to_string(),
1861            }
1862        }
1863        InjectionKind::ArgumentInjection => {
1864            format!(
1865                "WARNING: User input may be passed as command arguments.\n\
1866                 Fix: Validate and sanitize all inputs:\n\
1867                 - Whitelist allowed values where possible\n\
1868                 - Reject inputs containing suspicious characters (-, --, etc.)\n\
1869                 - Use -- to separate options from arguments\n\
1870                 - Consider using allowlists for filenames/paths",
1871            )
1872        }
1873    }
1874}
1875
1876/// Get text from a node, handling UTF-8 safely.
1877fn node_text<'a>(node: Node<'a>, source: &'a [u8]) -> &'a str {
1878    std::str::from_utf8(&source[node.start_byte()..node.end_byte()]).unwrap_or("")
1879}
1880
1881// =============================================================================
1882// Tests
1883// =============================================================================
1884
1885#[cfg(test)]
1886mod tests {
1887    use super::*;
1888    use std::io::Write;
1889    use tempfile::NamedTempFile;
1890
1891    fn create_temp_file(content: &str, extension: &str) -> NamedTempFile {
1892        let mut file = tempfile::Builder::new()
1893            .suffix(extension)
1894            .tempfile()
1895            .expect("Failed to create temp file");
1896        file.write_all(content.as_bytes()).expect("Failed to write");
1897        file
1898    }
1899
1900    // =========================================================================
1901    // Python Tests
1902    // =========================================================================
1903
1904    #[test]
1905    fn test_python_direct_os_system_injection() {
1906        let source = r#"
1907import os
1908
1909def handle_request(request):
1910    cmd = request.args['cmd']
1911    os.system(cmd)  # Direct injection
1912"#;
1913        let file = create_temp_file(source, ".py");
1914        let findings = scan_file_command_injection(file.path(), Some("python"))
1915            .expect("Scan should succeed");
1916
1917        assert!(!findings.is_empty(), "Should detect os.system vulnerability");
1918        let finding = &findings[0];
1919        assert_eq!(finding.sink_function, "system");
1920        assert_eq!(finding.kind, InjectionKind::CommandInjection);
1921        assert!(finding.severity >= Severity::High);
1922    }
1923
1924    #[test]
1925    fn test_python_indirect_os_system_injection() {
1926        let source = r#"
1927import os
1928
1929def handle_request(request):
1930    user_input = request.args.get('cmd')
1931    command = "ls -la " + user_input
1932    os.system(command)  # Indirect injection via concatenation
1933"#;
1934        let file = create_temp_file(source, ".py");
1935        let findings = scan_file_command_injection(file.path(), Some("python"))
1936            .expect("Scan should succeed");
1937
1938        assert!(!findings.is_empty(), "Should detect indirect injection");
1939        // Even without full taint tracking, os.system is a dangerous sink
1940        // The severity should be at least High due to shell_by_default
1941        assert!(findings[0].severity >= Severity::High);
1942    }
1943
1944    #[test]
1945    fn test_python_subprocess_shell_true() {
1946        let source = r#"
1947import subprocess
1948
1949def run_command(user_input):
1950    subprocess.run(user_input, shell=True)  # Dangerous!
1951"#;
1952        let file = create_temp_file(source, ".py");
1953        let findings = scan_file_command_injection(file.path(), Some("python"))
1954            .expect("Scan should succeed");
1955
1956        assert!(!findings.is_empty(), "Should detect subprocess with shell=True");
1957        let finding = &findings[0];
1958        assert_eq!(finding.kind, InjectionKind::CommandInjection);
1959    }
1960
1961    #[test]
1962    fn test_python_subprocess_list_args_safe() {
1963        let source = r#"
1964import subprocess
1965
1966def run_safe(filename):
1967    # Safe: using list args without shell
1968    subprocess.run(['cat', filename], shell=False)
1969"#;
1970        let file = create_temp_file(source, ".py");
1971        let findings = scan_file_command_injection(file.path(), Some("python"))
1972            .expect("Scan should succeed");
1973
1974        // Should still detect but with lower severity (argument injection possible)
1975        if !findings.is_empty() {
1976            assert!(findings[0].kind == InjectionKind::ArgumentInjection ||
1977                    findings[0].severity <= Severity::Medium);
1978        }
1979    }
1980
1981    #[test]
1982    fn test_python_eval_code_injection() {
1983        let source = r#"
1984def calculate(expression):
1985    return eval(expression)  # Code injection!
1986"#;
1987        let file = create_temp_file(source, ".py");
1988        let findings = scan_file_command_injection(file.path(), Some("python"))
1989            .expect("Scan should succeed");
1990
1991        assert!(!findings.is_empty(), "Should detect eval vulnerability");
1992        let finding = &findings[0];
1993        assert_eq!(finding.sink_function, "eval");
1994        assert_eq!(finding.kind, InjectionKind::CodeInjection);
1995        assert_eq!(finding.severity, Severity::Critical);
1996    }
1997
1998    #[test]
1999    fn test_python_exec_code_injection() {
2000        let source = r#"
2001def run_code(code):
2002    exec(code)  # Code injection!
2003"#;
2004        let file = create_temp_file(source, ".py");
2005        let findings = scan_file_command_injection(file.path(), Some("python"))
2006            .expect("Scan should succeed");
2007
2008        assert!(!findings.is_empty(), "Should detect exec vulnerability");
2009        assert_eq!(findings[0].kind, InjectionKind::CodeInjection);
2010    }
2011
2012    #[test]
2013    fn test_python_input_to_system() {
2014        let source = r#"
2015import os
2016
2017def main():
2018    cmd = input("Enter command: ")
2019    os.system(cmd)
2020"#;
2021        let file = create_temp_file(source, ".py");
2022        let findings = scan_file_command_injection(file.path(), Some("python"))
2023            .expect("Scan should succeed");
2024
2025        assert!(!findings.is_empty(), "Should detect input() to os.system");
2026    }
2027
2028    // =========================================================================
2029    // TypeScript/JavaScript Tests
2030    // =========================================================================
2031
2032    #[test]
2033    fn test_typescript_child_process_exec() {
2034        // Use child_process.exec pattern which matches the query
2035        let source = r#"
2036const child_process = require('child_process');
2037
2038function runCommand(userInput: string) {
2039    child_process.exec(userInput, (error, stdout, stderr) => {
2040        console.log(stdout);
2041    });
2042}
2043"#;
2044        let file = create_temp_file(source, ".ts");
2045        let findings = scan_file_command_injection(file.path(), Some("typescript"))
2046            .expect("Scan should succeed");
2047
2048        assert!(!findings.is_empty(), "Should detect child_process.exec");
2049        // Find the CommandInjection finding (there may be multiple findings)
2050        let cmd_injection = findings.iter().find(|f| f.kind == InjectionKind::CommandInjection);
2051        assert!(cmd_injection.is_some() || findings[0].sink_function == "exec",
2052            "Should detect command injection or exec sink");
2053    }
2054
2055    #[test]
2056    fn test_typescript_eval() {
2057        let source = r#"
2058function processUserCode(code: string) {
2059    return eval(code);  // Code injection!
2060}
2061"#;
2062        let file = create_temp_file(source, ".ts");
2063        let findings = scan_file_command_injection(file.path(), Some("typescript"))
2064            .expect("Scan should succeed");
2065
2066        assert!(!findings.is_empty(), "Should detect eval vulnerability");
2067        assert_eq!(findings[0].kind, InjectionKind::CodeInjection);
2068    }
2069
2070    #[test]
2071    fn test_typescript_spawn_shell_true() {
2072        let source = r#"
2073import { spawn } from 'child_process';
2074
2075function runWithShell(cmd: string) {
2076    spawn(cmd, { shell: true });
2077}
2078"#;
2079        let file = create_temp_file(source, ".ts");
2080        let findings = scan_file_command_injection(file.path(), Some("typescript"))
2081            .expect("Scan should succeed");
2082
2083        // Should detect spawn with shell:true
2084        if !findings.is_empty() {
2085            assert!(findings[0].severity >= Severity::High);
2086        }
2087    }
2088
2089    // =========================================================================
2090    // Go Tests
2091    // =========================================================================
2092
2093    #[test]
2094    fn test_go_exec_command() {
2095        let source = r#"
2096package main
2097
2098import (
2099    "os/exec"
2100)
2101
2102func runCommand(userInput string) {
2103    cmd := exec.Command(userInput)
2104    cmd.Run()
2105}
2106"#;
2107        let file = create_temp_file(source, ".go");
2108        let findings = scan_file_command_injection(file.path(), Some("go"))
2109            .expect("Scan should succeed");
2110
2111        assert!(!findings.is_empty(), "Should detect exec.Command with user input");
2112    }
2113
2114    // =========================================================================
2115    // C Tests
2116    // =========================================================================
2117
2118    #[test]
2119    fn test_c_system_call() {
2120        let source = r#"
2121#include <stdlib.h>
2122
2123void execute(char* userInput) {
2124    system(userInput);
2125}
2126"#;
2127        let file = create_temp_file(source, ".c");
2128        let findings = scan_file_command_injection(file.path(), Some("c"))
2129            .expect("Scan should succeed");
2130
2131        assert!(!findings.is_empty(), "Should detect system() call");
2132        assert_eq!(findings[0].kind, InjectionKind::CommandInjection);
2133        assert_eq!(findings[0].severity, Severity::Critical);
2134    }
2135
2136    #[test]
2137    fn test_c_popen() {
2138        let source = r#"
2139#include <stdio.h>
2140
2141void readOutput(char* cmd) {
2142    FILE* fp = popen(cmd, "r");
2143    pclose(fp);
2144}
2145"#;
2146        let file = create_temp_file(source, ".c");
2147        let findings = scan_file_command_injection(file.path(), Some("c"))
2148            .expect("Scan should succeed");
2149
2150        assert!(!findings.is_empty(), "Should detect popen() call");
2151    }
2152
2153    // =========================================================================
2154    // Rust Tests
2155    // =========================================================================
2156
2157    #[test]
2158    fn test_rust_command_new() {
2159        let source = r#"
2160use std::process::Command;
2161
2162fn run_command(user_input: &str) {
2163    Command::new(user_input)
2164        .spawn()
2165        .expect("failed");
2166}
2167"#;
2168        let file = create_temp_file(source, ".rs");
2169        let findings = scan_file_command_injection(file.path(), Some("rust"))
2170            .expect("Scan should succeed");
2171
2172        assert!(!findings.is_empty(), "Should detect Command::new with user input");
2173    }
2174
2175    // =========================================================================
2176    // Java Tests
2177    // =========================================================================
2178
2179    #[test]
2180    fn test_java_runtime_exec() {
2181        let source = r#"
2182public class CommandRunner {
2183    public void run(String userInput) throws Exception {
2184        Runtime.getRuntime().exec(userInput);
2185    }
2186}
2187"#;
2188        let file = create_temp_file(source, ".java");
2189        let findings = scan_file_command_injection(file.path(), Some("java"))
2190            .expect("Scan should succeed");
2191
2192        // Note: Java Runtime.exec detection depends on tree-sitter-java grammar details
2193        // This test verifies the scan completes without error; detection may vary
2194        if !findings.is_empty() {
2195            assert!(findings[0].sink_function.contains("exec"));
2196        }
2197    }
2198
2199    // =========================================================================
2200    // Utility Tests
2201    // =========================================================================
2202
2203    #[test]
2204    fn test_severity_ordering() {
2205        assert!(Severity::Critical > Severity::High);
2206        assert!(Severity::High > Severity::Medium);
2207        assert!(Severity::Medium > Severity::Low);
2208        assert!(Severity::Low > Severity::Info);
2209    }
2210
2211    #[test]
2212    fn test_confidence_ordering() {
2213        assert!(Confidence::High > Confidence::Medium);
2214        assert!(Confidence::Medium > Confidence::Low);
2215    }
2216
2217    #[test]
2218    fn test_get_command_sinks_coverage() {
2219        // Ensure we have sinks defined for all supported languages
2220        let languages = ["python", "typescript", "javascript", "go", "rust", "c", "cpp", "java"];
2221        for lang in languages {
2222            let sinks = get_command_sinks(lang);
2223            assert!(!sinks.is_empty(), "Should have sinks for {}", lang);
2224        }
2225    }
2226
2227    #[test]
2228    fn test_classify_taint_source() {
2229        assert_eq!(
2230            classify_taint_source("request.args", "python"),
2231            TaintSourceKind::HttpRequest
2232        );
2233        assert_eq!(
2234            classify_taint_source("request.body", "python"),
2235            TaintSourceKind::FormInput
2236        );
2237        assert_eq!(
2238            classify_taint_source("os.environ", "python"),
2239            TaintSourceKind::EnvVar
2240        );
2241        assert_eq!(
2242            classify_taint_source("sys.argv", "python"),
2243            TaintSourceKind::CmdLineArg
2244        );
2245        assert_eq!(
2246            classify_taint_source("sys.stdin", "python"),
2247            TaintSourceKind::StdIn
2248        );
2249    }
2250
2251    #[test]
2252    fn test_injection_kind_display() {
2253        assert_eq!(
2254            format!("{}", InjectionKind::CommandInjection),
2255            "command_injection"
2256        );
2257        assert_eq!(
2258            format!("{}", InjectionKind::ArgumentInjection),
2259            "argument_injection"
2260        );
2261        assert_eq!(
2262            format!("{}", InjectionKind::CodeInjection),
2263            "code_injection"
2264        );
2265    }
2266
2267    #[test]
2268    fn test_source_location_display() {
2269        let loc = SourceLocation {
2270            file: "test.py".to_string(),
2271            line: 10,
2272            column: 5,
2273            end_line: 10,
2274            end_column: 20,
2275        };
2276        assert_eq!(format!("{}", loc), "test.py:10:5");
2277    }
2278}