sandbox_runtime/utils/
ripgrep.rs

1//! Ripgrep integration for dangerous file discovery.
2
3use std::path::Path;
4use std::process::Command;
5
6use crate::config::{RipgrepConfig, DANGEROUS_DIRECTORIES, DANGEROUS_FILES};
7use crate::error::SandboxError;
8
9/// Default search depth for mandatory deny discovery.
10pub const DEFAULT_SEARCH_DEPTH: u32 = 3;
11
12/// Find dangerous files in a directory using ripgrep.
13/// Returns a list of absolute paths to dangerous files/directories.
14pub fn find_dangerous_files(
15    cwd: &Path,
16    config: Option<&RipgrepConfig>,
17    max_depth: Option<u32>,
18) -> Result<Vec<String>, SandboxError> {
19    let rg_config = config.cloned().unwrap_or_default();
20    let depth = max_depth.unwrap_or(DEFAULT_SEARCH_DEPTH);
21
22    let mut cmd = Command::new(&rg_config.command);
23
24    // Basic flags
25    cmd.arg("--files")
26        .arg("--hidden")
27        .arg("--max-depth")
28        .arg(depth.to_string());
29
30    // Add iglob patterns for dangerous files (case-insensitive)
31    for file in DANGEROUS_FILES {
32        cmd.arg("--iglob").arg(format!("**/{}", file));
33    }
34
35    // Add glob patterns for dangerous directories
36    for dir in DANGEROUS_DIRECTORIES {
37        cmd.arg("--iglob").arg(format!("**/{}/**", dir));
38    }
39
40    // Exclude node_modules to speed up search
41    cmd.arg("-g").arg("!**/node_modules/**");
42
43    // Add any custom args
44    if let Some(ref args) = rg_config.args {
45        for arg in args {
46            cmd.arg(arg);
47        }
48    }
49
50    // Set the working directory
51    cmd.arg(cwd);
52
53    // Execute the command
54    let output = cmd.output().map_err(|e| {
55        if e.kind() == std::io::ErrorKind::NotFound {
56            SandboxError::MissingDependency(format!(
57                "ripgrep not found at '{}'. Please install ripgrep.",
58                rg_config.command
59            ))
60        } else {
61            SandboxError::Io(e)
62        }
63    })?;
64
65    // Parse the output
66    let stdout = String::from_utf8_lossy(&output.stdout);
67    let files: Vec<String> = stdout
68        .lines()
69        .filter(|line| !line.is_empty())
70        .map(|line| {
71            // Ensure paths are absolute
72            let path = Path::new(line);
73            if path.is_absolute() {
74                line.to_string()
75            } else {
76                cwd.join(line).display().to_string()
77            }
78        })
79        .collect();
80
81    Ok(files)
82}
83
84/// Check if ripgrep is available.
85pub fn check_ripgrep(config: Option<&RipgrepConfig>) -> bool {
86    let command = config.map(|c| c.command.as_str()).unwrap_or("rg");
87
88    Command::new(command)
89        .arg("--version")
90        .output()
91        .map(|o| o.status.success())
92        .unwrap_or(false)
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn test_check_ripgrep() {
101        // This test will pass if ripgrep is installed
102        let available = check_ripgrep(None);
103        // We don't assert the result since it depends on the environment
104        println!("Ripgrep available: {}", available);
105    }
106}