1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
52#[serde(rename_all = "lowercase")]
53pub enum Severity {
54 Info,
56 Low,
58 Medium,
60 High,
62 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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
80#[serde(rename_all = "lowercase")]
81pub enum Confidence {
82 Low,
84 Medium,
86 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
102#[serde(rename_all = "snake_case")]
103pub enum InjectionKind {
104 CommandInjection,
106 ArgumentInjection,
108 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#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
124pub struct SourceLocation {
125 pub file: String,
127 pub line: usize,
129 pub column: usize,
131 pub end_line: usize,
133 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
145#[serde(rename_all = "snake_case")]
146pub enum TaintSourceKind {
147 HttpRequest,
149 FormInput,
151 StdIn,
153 FileRead,
155 EnvVar,
157 CmdLineArg,
159 DatabaseResult,
161 NetworkData,
163 UserConfig,
165 Unknown,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct TaintSource {
172 pub kind: TaintSourceKind,
174 pub variable: String,
176 pub location: SourceLocation,
178 pub description: String,
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct CommandSink {
185 pub language: String,
187 pub module: Option<String>,
189 pub function: String,
191 pub command_arg_index: usize,
193 pub shell_by_default: bool,
195 pub severity: Severity,
197 pub description: String,
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct CommandInjectionFinding {
204 pub location: SourceLocation,
206 pub severity: Severity,
208 pub sink_function: String,
210 pub tainted_input: String,
212 pub confidence: Confidence,
214 pub kind: InjectionKind,
216 #[serde(skip_serializing_if = "Vec::is_empty")]
218 pub taint_chain: Vec<TaintSource>,
219 #[serde(skip_serializing_if = "Option::is_none")]
221 pub code_snippet: Option<String>,
222 pub remediation: String,
224}
225
226pub 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
952fn 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
969const 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
1010const 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
1048const 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
1079const 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
1100const 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
1121const 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
1150fn 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
1167const 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
1199const 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
1237const 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
1258const 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
1282const 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
1300const 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
1331pub 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 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
1371pub fn scan_file_command_injection(file: &Path, language: Option<&str>) -> Result<Vec<CommandInjectionFinding>> {
1382 let registry = LanguageRegistry::global();
1383
1384 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 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 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 let sinks = find_sinks(&tree, &source, &ts_lang, sink_query_str, lang_name, &file_path)?;
1420
1421 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 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#[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
1457fn 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 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
1525fn 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 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
1579fn 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
1602fn extract_assigned_variable(node: Node, source: &[u8]) -> Option<String> {
1604 let mut current = node;
1606 while let Some(parent) = current.parent() {
1607 match parent.kind() {
1608 "assignment" | "assignment_statement" | "variable_declaration" | "lexical_declaration" => {
1609 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 if child.end_byte() < node.start_byte() {
1617 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
1632fn 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 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 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 let severity = if kind == InjectionKind::CodeInjection {
1664 Severity::Critical
1666 } else if sink_def.shell_by_default || sink.has_shell_true {
1667 if confidence >= Confidence::Medium {
1669 Severity::Critical
1670 } else {
1671 sink_def.severity
1672 }
1673 } else if confidence == Confidence::High {
1674 Severity::High
1676 } else if confidence == Confidence::Medium {
1677 Severity::Medium
1678 } else {
1679 sink_def.severity.min(Severity::Low)
1681 };
1682
1683 let code_snippet = extract_code_snippet(source, location);
1685
1686 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
1702fn 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 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 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 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 !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
1763fn 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
1774fn 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 if child.kind() == "(" || child.kind() == ")" || child.kind() == "," {
1780 continue;
1781 }
1782 return Some(node_text(child, source).to_string());
1784 }
1785 None
1786}
1787
1788fn 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 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
1806fn 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
1876fn 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#[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 #[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 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 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 #[test]
2033 fn test_typescript_child_process_exec() {
2034 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 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 if !findings.is_empty() {
2085 assert!(findings[0].severity >= Severity::High);
2086 }
2087 }
2088
2089 #[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 #[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 #[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 #[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 if !findings.is_empty() {
2195 assert!(findings[0].sink_function.contains("exec"));
2196 }
2197 }
2198
2199 #[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 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}