Skip to main content

vtcode_safety/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::{EvaluationReason, EvaluationResult, PolicyAwareEvaluator, UnifiedCommandEvaluator};
43#[cfg(windows)]
44pub use windows_cmdlet_db::{CmdletCategory, CmdletDatabase, CmdletInfo, CmdletSeverity};
45#[cfg(windows)]
46pub use windows_com_analyzer::{ComObjectAnalyzer, ComObjectContext, ComObjectInfo, ComRiskLevel};
47#[cfg(windows)]
48pub use windows_enhanced::is_dangerous_windows_enhanced;
49#[cfg(windows)]
50pub use windows_registry_filter::{RegistryAccessFilter, RegistryAccessPattern, RegistryPathInfo, RegistryRiskLevel};
51
52/// Evaluates if a command is safe to execute.
53/// Returns true if the command passes all safety checks.
54fn is_safe_command(registry: &SafeCommandRegistry, command: &[String]) -> bool {
55    if command.is_empty() {
56        return false;
57    }
58
59    // Check dangerous commands first
60    if command_might_be_dangerous(command) {
61        return false;
62    }
63
64    // Check safe command registry
65    matches!(registry.is_safe(command), SafetyDecision::Allow)
66}
67
68/// Evaluate a shell command string by parsing it into subcommands and checking
69/// each with the centralized dangerous-command detector.
70///
71/// Falls back to whitespace tokenization when structured parsing fails.
72pub fn shell_string_might_be_dangerous(command: &str) -> bool {
73    if let Ok(parsed_commands) = shell_parser::parse_shell_commands(command)
74        && parsed_commands
75            .iter()
76            .any(|cmd| !cmd.is_empty() && command_might_be_dangerous(cmd))
77    {
78        return true;
79    }
80
81    let fallback_tokens: Vec<String> = command.split_whitespace().map(ToString::to_string).collect();
82    !fallback_tokens.is_empty() && command_might_be_dangerous(&fallback_tokens)
83}
84
85/// Validates that a command is safe to execute.
86///
87/// Combines the centralized dangerous-command detector with injection pattern
88/// detection and additional dangerous-pattern checks (wget, curl, rmdir, etc.).
89/// This is the single entry point for command safety validation.
90pub fn validate_command_safety(command: &str) -> anyhow::Result<()> {
91    use anyhow::bail;
92
93    if command.len() < 3 {
94        return Ok(());
95    }
96
97    if shell_parser::contains_dynamic_find_syntax(command) {
98        bail!("dynamic shell expansion in find commands is not allowed");
99    }
100
101    let segments = shell_parser::split_shell_segments(command)?;
102
103    if shell_string_might_be_dangerous(command) {
104        bail!("Potential dangerous command detected");
105    }
106
107    for segment in segments {
108        if let Some(pattern) = shell_parser::additional_dangerous_pattern(&segment) {
109            bail!("Potential dangerous command: {pattern}");
110        }
111    }
112
113    Ok(())
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn empty_command_is_not_safe() {
122        let registry = SafeCommandRegistry::new();
123        assert!(!is_safe_command(&registry, &[]));
124    }
125
126    #[test]
127    fn shell_string_detects_dangerous_sequence() {
128        assert!(shell_string_might_be_dangerous("echo ok && git reset --hard HEAD~1"));
129    }
130
131    #[test]
132    fn validation_rejects_dynamic_find_option_splicing() {
133        assert!(validate_command_safety("find src -maxdepth 0 -exe$''c touch /tmp/VT_BYPASS_POC {} +").is_err());
134    }
135}