Skip to main content

vtcode_core/command_safety/
mod.rs

1//! Command safety detection module
2//!
3//! Implements granular command safety evaluation based on subcommands and options,
4//! following patterns from OpenAI's Codex project.
5//!
6//! Features:
7//! - Safe-by-default subcommand allowlists (e.g., `git` only allows `branch|status|log`)
8//! - Per-option blacklists (e.g., `find` forbids `-delete`, `-exec`)
9//! - Shell chain parsing for `bash -lc "..."` scripts
10//! - Windows/PowerShell-specific dangerous command detection
11//! - Recursive dangerous command detection with `sudo` unwrapping
12//! - Audit logging for compliance
13//! - LRU caching for performance
14
15pub mod audit;
16pub mod cache;
17pub mod command_db;
18pub mod dangerous_commands;
19pub mod safe_command_registry;
20pub mod shell_parser;
21pub mod unified;
22#[cfg(windows)]
23pub mod windows;
24#[cfg(windows)]
25pub mod windows_cmdlet_db;
26#[cfg(windows)]
27pub mod windows_com_analyzer;
28#[cfg(windows)]
29pub mod windows_enhanced;
30#[cfg(windows)]
31pub mod windows_registry_filter;
32
33#[cfg(test)]
34mod integration_tests;
35
36pub use audit::{AuditEntry, SafetyAuditLogger};
37pub use cache::SafetyDecisionCache;
38pub use command_db::CommandDatabase;
39pub use dangerous_commands::command_might_be_dangerous;
40pub use safe_command_registry::{SafeCommandRegistry, SafetyDecision};
41pub use shell_parser::parse_bash_lc_commands;
42pub use unified::{
43    EvaluationReason, EvaluationResult, PolicyAwareEvaluator, UnifiedCommandEvaluator,
44};
45#[cfg(windows)]
46pub use windows_cmdlet_db::{CmdletCategory, CmdletDatabase, CmdletInfo, CmdletSeverity};
47#[cfg(windows)]
48pub use windows_com_analyzer::{ComObjectAnalyzer, ComObjectContext, ComObjectInfo, ComRiskLevel};
49#[cfg(windows)]
50pub use windows_enhanced::is_dangerous_windows_enhanced;
51#[cfg(windows)]
52pub use windows_registry_filter::{
53    RegistryAccessFilter, RegistryAccessPattern, RegistryPathInfo, RegistryRiskLevel,
54};
55
56/// Evaluates if a command is safe to execute.
57/// Returns true if the command passes all safety checks.
58pub fn is_safe_command(registry: &SafeCommandRegistry, command: &[String]) -> bool {
59    if command.is_empty() {
60        return false;
61    }
62
63    // Check dangerous commands first
64    if command_might_be_dangerous(command) {
65        return false;
66    }
67
68    // Check safe command registry
69    matches!(registry.is_safe(command), SafetyDecision::Allow)
70}
71
72/// Evaluate a shell command string by parsing it into subcommands and checking
73/// each with the centralized dangerous-command detector.
74///
75/// Falls back to whitespace tokenization when structured parsing fails.
76pub fn shell_string_might_be_dangerous(command: &str) -> bool {
77    if let Ok(parsed_commands) = shell_parser::parse_shell_commands(command)
78        && parsed_commands
79            .iter()
80            .any(|cmd| !cmd.is_empty() && command_might_be_dangerous(cmd))
81    {
82        return true;
83    }
84
85    let fallback_tokens: Vec<String> = command
86        .split_whitespace()
87        .map(ToString::to_string)
88        .collect();
89    !fallback_tokens.is_empty() && command_might_be_dangerous(&fallback_tokens)
90}
91
92/// Validates that a command is safe to execute.
93///
94/// Combines the centralized dangerous-command detector with injection pattern
95/// detection and additional dangerous-pattern checks (wget, curl, rmdir, etc.).
96/// This is the single entry point for command safety validation.
97pub fn validate_command_safety(command: &str) -> anyhow::Result<()> {
98    use anyhow::bail;
99
100    if command.len() < 3 {
101        return Ok(());
102    }
103
104    let segments = shell_parser::split_shell_segments(command)?;
105
106    if shell_string_might_be_dangerous(command) {
107        bail!("Potential dangerous command detected");
108    }
109
110    for segment in segments {
111        if let Some(pattern) = shell_parser::additional_dangerous_pattern(&segment) {
112            bail!("Potential dangerous command: {pattern}");
113        }
114    }
115
116    Ok(())
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn empty_command_is_not_safe() {
125        let registry = SafeCommandRegistry::new();
126        assert!(!is_safe_command(&registry, &[]));
127    }
128
129    #[test]
130    fn shell_string_detects_dangerous_sequence() {
131        assert!(shell_string_might_be_dangerous(
132            "echo ok && git reset --hard HEAD~1"
133        ));
134    }
135}