vtcode_safety/command_safety/
mod.rs1pub 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
52fn is_safe_command(registry: &SafeCommandRegistry, command: &[String]) -> bool {
55 if command.is_empty() {
56 return false;
57 }
58
59 if command_might_be_dangerous(command) {
61 return false;
62 }
63
64 matches!(registry.is_safe(command), SafetyDecision::Allow)
66}
67
68pub 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
85pub 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(®istry, &[]));
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}