use regex::Regex;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use std::collections::HashSet;
use std::sync::Arc;
use thiserror::Error;
use tree_sitter::Parser;
use tree_sitter::Tree;
use tree_sitter_bash::LANGUAGE as BASH;
#[derive(Error, Debug, Clone, PartialEq)]
#[allow(dead_code)]
pub enum BashParseError {
#[error("Failed to parse bash syntax: {message}")]
ParseError { message: String },
#[error("Dangerous pattern detected: {pattern} in command: {command}")]
DangerousPattern { pattern: String, command: String },
#[error("Command not allowed: {command}")]
ForbiddenCommand { command: String },
#[error("Unsafe context detected: {reason}")]
UnsafeContext { reason: String },
#[error("Rewriting failed: {reason}")]
RewriteError { reason: String },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ConfidenceLevel {
Low, Medium, High, VeryHigh, }
impl ConfidenceLevel {
#[allow(dead_code)]
pub fn from_score(score: f32) -> Self {
match score {
s if s <= 0.40 => Self::Low,
s if s <= 0.70 => Self::Medium,
s if s <= 0.90 => Self::High,
_ => Self::VeryHigh,
}
}
#[allow(dead_code)]
pub const fn as_score(&self) -> f32 {
match self {
Self::Low => 0.25,
Self::Medium => 0.55,
Self::High => 0.80,
Self::VeryHigh => 0.95,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AppliedRewrite {
pub original_pattern: String,
pub replacement: String,
pub reason: String,
pub position: usize,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SecurityAnalysis {
pub threat_level: ThreatLevel,
pub detected_patterns: Vec<String>,
pub risk_factors: Vec<String>,
pub recommendations: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ThreatLevel {
Safe,
Low,
Medium,
High,
Critical,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SafeCommand {
pub commands: Vec<Vec<String>>,
pub original_command: String,
pub final_command: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SafeCommandReport {
pub safe_command: SafeCommand,
pub security_analysis: SecurityAnalysis,
pub applied_rewrites: Vec<AppliedRewrite>,
pub confidence_score: f32,
pub confidence_level: ConfidenceLevel,
pub validation_time_ms: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SandboxRule {
pub name: String,
pub description: String,
pub pattern: String,
pub action: SandboxAction,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum SandboxAction {
Block,
Rewrite { replacement: String },
Restrict { allowed_paths: Vec<String> },
RequireApproval,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct CommandValidator {
dangerous_patterns: Vec<Regex>,
allowed_commands: HashSet<String>,
context_rules: Vec<SandboxRule>,
}
impl CommandValidator {
pub fn new() -> Self {
let dangerous_patterns = vec![
Regex::new(r"[;&|`$(){}\[\]<>]").unwrap(),
Regex::new(r"(rm|del|delete).*-r").unwrap(),
Regex::new(r"(chmod|chown).*777").unwrap(),
Regex::new(r"(curl|wget|nc|netcat).*\|").unwrap(),
Regex::new(r"(sudo|su)\s").unwrap(),
Regex::new(r"\.\./.*\.\./").unwrap(),
];
let allowed_commands = [
"ls", "cat", "head", "tail", "less", "more", "file", "stat", "find", "grep", "awk",
"sed", "sort", "uniq", "wc", "cut", "pwd", "cd", "mkdir", "rmdir", "echo", "printf", "tr", "diff", "comm", "join",
"tar", "gzip", "gunzip", "zip", "unzip", "git", "cargo", "npm", "python", "python3", "node", "rustc", "gcc", "clang", "make",
"cmake", "ps", "top", "df", "du", "free", "uname", "whoami", "id", "date", "uptime", "env",
"printenv", "true", "false", "yes", "sleep", "timeout", "apt", "yum", "brew", "pacman",
]
.iter()
.map(|&s| s.to_string())
.collect();
let context_rules = vec![
SandboxRule {
name: "no_root_commands".to_string(),
description: "Block commands that require root access".to_string(),
pattern: r"^(sudo|su|doas)\s".to_string(),
action: SandboxAction::Block,
},
SandboxRule {
name: "safe_rm".to_string(),
description: "Rewrite dangerous rm commands".to_string(),
pattern: r"rm\s+(-rf?|--recursive)\s+/".to_string(),
action: SandboxAction::Block,
},
];
Self {
dangerous_patterns,
allowed_commands,
context_rules,
}
}
#[allow(dead_code)]
pub fn validate_command(&self, command: &[String]) -> Result<SecurityAnalysis, BashParseError> {
if command.is_empty() {
return Ok(SecurityAnalysis {
threat_level: ThreatLevel::Safe,
detected_patterns: vec![],
risk_factors: vec![],
recommendations: vec![],
});
}
let cmd_name = &command[0];
let full_command = command.join(" ");
let mut detected_patterns = Vec::new();
let mut risk_factors = Vec::new();
let mut recommendations = Vec::new();
if !self.allowed_commands.contains(cmd_name) {
return Err(BashParseError::ForbiddenCommand {
command: cmd_name.clone(),
});
}
for pattern in &self.dangerous_patterns {
if pattern.is_match(&full_command) {
detected_patterns.push(pattern.as_str().to_string());
}
}
let threat_level = if !detected_patterns.is_empty() {
risk_factors.push("Command contains potentially dangerous patterns".to_string());
recommendations.push("Review command arguments for security implications".to_string());
ThreatLevel::Medium
} else if self.is_high_risk_command(cmd_name) {
risk_factors.push("Command has elevated privileges or system access".to_string());
recommendations.push("Use with caution in production environments".to_string());
ThreatLevel::Low
} else {
ThreatLevel::Safe
};
Ok(SecurityAnalysis {
threat_level,
detected_patterns,
risk_factors,
recommendations,
})
}
#[allow(dead_code)]
fn is_high_risk_command(&self, command: &str) -> bool {
matches!(
command,
"rm" | "mv" | "cp" | "chmod" | "chown" | "git" | "cargo" | "npm"
)
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct CommandRewriter {
rewrite_rules: HashMap<String, String>,
}
impl CommandRewriter {
pub fn new() -> Self {
let mut rewrite_rules = HashMap::new();
rewrite_rules.insert("rm -rf".to_string(), "rm -r".to_string());
rewrite_rules.insert("chmod 777".to_string(), "chmod 755".to_string());
rewrite_rules.insert("chown -R".to_string(), "chown".to_string());
Self { rewrite_rules }
}
#[allow(dead_code)]
pub fn rewrite_command(&self, command: &mut Vec<String>) -> Vec<AppliedRewrite> {
let mut applied_rewrites = Vec::new();
let _original_command = command.join(" ");
for (pattern, replacement) in &self.rewrite_rules {
let original_joined = command.join(" ");
if original_joined.contains(pattern) {
let new_command = original_joined.replace(pattern, replacement);
let new_parts: Vec<String> = new_command
.split_whitespace()
.map(|s| s.to_string())
.collect();
if new_parts != *command {
applied_rewrites.push(AppliedRewrite {
original_pattern: pattern.clone(),
replacement: replacement.clone(),
reason: "Security enhancement".to_string(),
position: original_joined.find(pattern).unwrap_or(0),
});
*command = new_parts;
}
}
}
applied_rewrites
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct SandboxRules {
rules: Vec<SandboxRule>,
}
impl SandboxRules {
pub fn new() -> Self {
let rules = vec![SandboxRule {
name: "workspace_only".to_string(),
description: "Restrict file operations to workspace directory".to_string(),
pattern: r"^(/|~|\.\./)".to_string(),
action: SandboxAction::Restrict {
allowed_paths: vec!["./*".to_string(), "./target/*".to_string()],
},
}];
Self { rules }
}
#[allow(dead_code)]
pub fn apply_rules(&self, command: &[String]) -> Result<(), BashParseError> {
let full_command = command.join(" ");
for rule in &self.rules {
let pattern = Regex::new(&rule.pattern).map_err(|e| BashParseError::UnsafeContext {
reason: format!("Invalid rule pattern: {}", e),
})?;
if pattern.is_match(&full_command) {
match &rule.action {
SandboxAction::Block => {
return Err(BashParseError::UnsafeContext {
reason: format!("Blocked by rule: {}", rule.name),
});
}
SandboxAction::RequireApproval => {
return Err(BashParseError::UnsafeContext {
reason: format!("Requires approval: {}", rule.description),
});
}
SandboxAction::Rewrite { replacement: _ } => {
}
SandboxAction::Restrict { allowed_paths: _ } => {
}
}
}
}
Ok(())
}
}
#[derive(Clone)]
#[allow(dead_code)]
pub struct EnhancedBashParser {
parser: Arc<std::sync::Mutex<Parser>>,
validator: CommandValidator,
sandbox_rules: SandboxRules,
rewriter: CommandRewriter,
}
impl std::fmt::Debug for EnhancedBashParser {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EnhancedBashParser")
.field("validator", &self.validator)
.field("sandbox_rules", &self.sandbox_rules)
.field("rewriter", &self.rewriter)
.field("parser", &"<tree_sitter::Parser>")
.finish()
}
}
impl EnhancedBashParser {
pub fn new() -> Result<Self, BashParseError> {
let lang = BASH.into();
let mut parser = Parser::new();
parser
.set_language(&lang)
.map_err(|e| BashParseError::ParseError {
message: format!("Failed to load bash grammar: {}", e),
})?;
Ok(Self {
parser: Arc::new(std::sync::Mutex::new(parser)),
validator: CommandValidator::new(),
sandbox_rules: SandboxRules::new(),
rewriter: CommandRewriter::new(),
})
}
#[allow(dead_code)]
pub fn parse_and_validate(&self, cmd: &str) -> Result<SafeCommandReport, BashParseError> {
let start_time = std::time::Instant::now();
let tree = self.parse_bash(cmd)?;
let mut commands = self.extract_commands(&tree, cmd)?;
let mut all_rewrites = Vec::new();
for command in &mut commands {
let rewrites = self.rewriter.rewrite_command(command);
all_rewrites.extend(rewrites);
}
let mut security_analyses = Vec::new();
for command in &commands {
let analysis = self.validator.validate_command(command)?;
security_analyses.push(analysis);
}
for command in &commands {
self.sandbox_rules.apply_rules(command)?;
}
let final_command = commands
.iter()
.map(|cmd| cmd.join(" "))
.collect::<Vec<_>>()
.join(" && ");
let overall_security_analysis = self.merge_security_analyses(security_analyses);
let confidence_score =
self.calculate_confidence_score(&commands, &overall_security_analysis);
let validation_time_ms = start_time.elapsed().as_millis() as u64;
Ok(SafeCommandReport {
safe_command: SafeCommand {
commands,
original_command: cmd.to_string(),
final_command,
},
security_analysis: overall_security_analysis,
applied_rewrites: all_rewrites,
confidence_score,
confidence_level: ConfidenceLevel::from_score(confidence_score),
validation_time_ms,
})
}
#[allow(dead_code)]
fn parse_bash(&self, cmd: &str) -> Result<Tree, BashParseError> {
let mut parser = self.parser.lock().map_err(|_| BashParseError::ParseError {
message: "Failed to acquire parser lock".to_string(),
})?;
parser
.parse(cmd, None)
.ok_or_else(|| BashParseError::ParseError {
message: "Failed to parse bash command".to_string(),
})
}
#[allow(dead_code)]
fn extract_commands(&self, tree: &Tree, src: &str) -> Result<Vec<Vec<String>>, BashParseError> {
try_parse_word_only_commands_sequence(tree, src).ok_or_else(|| BashParseError::ParseError {
message: "Command contains unsupported constructs".to_string(),
})
}
#[allow(dead_code)]
fn merge_security_analyses(&self, analyses: Vec<SecurityAnalysis>) -> SecurityAnalysis {
let mut detected_patterns = Vec::new();
let mut risk_factors = Vec::new();
let mut recommendations = Vec::new();
let mut max_threat_level = ThreatLevel::Safe;
for analysis in analyses {
detected_patterns.extend(analysis.detected_patterns);
risk_factors.extend(analysis.risk_factors);
recommendations.extend(analysis.recommendations);
if self.threat_level_severity(analysis.threat_level)
> self.threat_level_severity(max_threat_level)
{
max_threat_level = analysis.threat_level;
}
}
detected_patterns.sort();
detected_patterns.dedup();
risk_factors.sort();
risk_factors.dedup();
recommendations.sort();
recommendations.dedup();
SecurityAnalysis {
threat_level: max_threat_level,
detected_patterns,
risk_factors,
recommendations,
}
}
#[allow(dead_code)]
const fn threat_level_severity(&self, level: ThreatLevel) -> u8 {
match level {
ThreatLevel::Safe => 0,
ThreatLevel::Low => 1,
ThreatLevel::Medium => 2,
ThreatLevel::High => 3,
ThreatLevel::Critical => 4,
}
}
#[allow(dead_code)]
fn calculate_confidence_score(
&self,
commands: &[Vec<String>],
analysis: &SecurityAnalysis,
) -> f32 {
let mut score = 1.0;
score -= analysis.detected_patterns.len() as f32 * 0.1;
score -= analysis.risk_factors.len() as f32 * 0.05;
score *= match analysis.threat_level {
ThreatLevel::Safe => 1.0,
ThreatLevel::Low => 0.9,
ThreatLevel::Medium => 0.7,
ThreatLevel::High => 0.5,
ThreatLevel::Critical => 0.2,
};
if commands.len() > 3 {
score *= 0.8;
}
score.clamp(0.0, 1.0)
}
}
impl Default for EnhancedBashParser {
fn default() -> Self {
Self::new().expect("Failed to create default EnhancedBashParser")
}
}
pub fn try_parse_bash(bash_lc_arg: &str) -> Option<Tree> {
let lang = BASH.into();
let mut parser = Parser::new();
#[expect(clippy::expect_used)]
parser.set_language(&lang).expect("load bash grammar");
let old_tree: Option<&Tree> = None;
parser.parse(bash_lc_arg, old_tree)
}
pub fn try_parse_word_only_commands_sequence(tree: &Tree, src: &str) -> Option<Vec<Vec<String>>> {
if tree.root_node().has_error() {
return None;
}
const ALLOWED_KINDS: &[&str] = &[
"program",
"list",
"pipeline",
"negated_command",
"command",
"command_name",
"word",
"string",
"string_content",
"raw_string",
"number",
"concatenation",
"simple_expansion",
"expansion",
"if_statement",
"then_clause",
"else_clause",
"elif_clause",
"case_statement",
"case_item",
"for_statement",
"while_statement",
"do_group",
"function_definition",
"compound_statement",
"file_descriptor",
"heredoc_start",
"heredoc_body",
"comment",
];
const ALLOWED_PUNCT_TOKENS: &[&str] = &["&&", "||", ";", "|", "\"", "'"];
let root = tree.root_node();
let mut cursor = root.walk();
let mut stack = vec![root];
let mut command_nodes = Vec::new();
while let Some(node) = stack.pop() {
let kind = node.kind();
if node.is_named() {
if !ALLOWED_KINDS.contains(&kind) {
return None;
}
if kind == "command" {
command_nodes.push(node);
}
} else {
if kind.chars().any(|c| "&;|".contains(c)) && !ALLOWED_PUNCT_TOKENS.contains(&kind) {
return None;
}
if !(ALLOWED_PUNCT_TOKENS.contains(&kind) || kind.trim().is_empty()) {
return None;
}
}
for child in node.children(&mut cursor) {
stack.push(child);
}
}
let mut commands = Vec::new();
for node in command_nodes {
if let Some(words) = parse_plain_command_from_node(node, src) {
commands.push(words);
} else {
return None;
}
}
Some(commands)
}
fn parse_plain_command_from_node(cmd: tree_sitter::Node, src: &str) -> Option<Vec<String>> {
if cmd.kind() != "command" {
return None;
}
let mut words = Vec::new();
let mut cursor = cmd.walk();
for child in cmd.named_children(&mut cursor) {
match child.kind() {
"command_name" => {
let word_node = child.named_child(0)?;
if word_node.kind() != "word" {
return None;
}
words.push(word_node.utf8_text(src.as_bytes()).ok()?.to_owned());
}
"word" | "number" => {
words.push(child.utf8_text(src.as_bytes()).ok()?.to_owned());
}
"string" => {
if child.child_count() == 3
&& child.child(0)?.kind() == "\""
&& child.child(1)?.kind() == "string_content"
&& child.child(2)?.kind() == "\""
{
words.push(child.child(1)?.utf8_text(src.as_bytes()).ok()?.to_owned());
} else {
return None;
}
}
"raw_string" => {
let raw_string = child.utf8_text(src.as_bytes()).ok()?;
let stripped = raw_string
.strip_prefix('\'')
.and_then(|s| s.strip_suffix('\''));
if let Some(s) = stripped {
words.push(s.to_owned());
} else {
return None;
}
}
_ => return None,
}
}
Some(words)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
fn parse_seq(src: &str) -> Option<Vec<Vec<String>>> {
let tree = try_parse_bash(src)?;
try_parse_word_only_commands_sequence(&tree, src)
}
#[test]
fn accepts_single_simple_command() {
let cmds = parse_seq("ls -1").unwrap();
assert_eq!(cmds, vec![vec!["ls".to_string(), "-1".to_string()]]);
}
#[test]
fn accepts_multiple_commands_with_allowed_operators() {
let src = "ls && pwd; echo 'hi there' | wc -l";
let cmds = parse_seq(src).unwrap();
let expected: Vec<Vec<String>> = vec![
vec!["wc".to_string(), "-l".to_string()],
vec!["echo".to_string(), "hi there".to_string()],
vec!["pwd".to_string()],
vec!["ls".to_string()],
];
assert_eq!(cmds, expected);
}
#[test]
fn extracts_double_and_single_quoted_strings() {
let cmds = parse_seq("echo \"hello world\"").unwrap();
assert_eq!(
cmds,
vec![vec!["echo".to_string(), "hello world".to_string()]]
);
let cmds2 = parse_seq("echo 'hi there'").unwrap();
assert_eq!(
cmds2,
vec![vec!["echo".to_string(), "hi there".to_string()]]
);
}
#[test]
fn accepts_numbers_as_words() {
let cmds = parse_seq("echo 123 456").unwrap();
assert_eq!(
cmds,
vec![vec![
"echo".to_string(),
"123".to_string(),
"456".to_string()
]]
);
}
#[test]
fn rejects_parentheses_and_subshells() {
assert!(parse_seq("(ls)").is_none());
assert!(parse_seq("ls || (pwd && echo hi)").is_none());
}
#[test]
fn rejects_redirections_and_unsupported_operators() {
assert!(parse_seq("ls > out.txt").is_none());
assert!(parse_seq("echo hi & echo bye").is_none());
}
#[test]
fn rejects_command_and_process_substitutions_and_expansions() {
assert!(parse_seq("echo $(pwd)").is_none());
assert!(parse_seq("echo `pwd`").is_none());
assert!(parse_seq("echo $HOME").is_none());
assert!(parse_seq("echo \"hi $USER\"").is_none());
}
#[test]
fn rejects_variable_assignment_prefix() {
assert!(parse_seq("FOO=bar ls").is_none());
}
#[test]
fn rejects_trailing_operator_parse_error() {
assert!(parse_seq("ls &&").is_none());
}
#[test]
fn enhanced_parser_creation() {
let parser = EnhancedBashParser::new();
assert!(parser.is_ok(), "Enhanced parser should create successfully");
}
#[test]
fn enhanced_parser_validates_safe_command() {
let parser = EnhancedBashParser::new().unwrap();
let result = parser.parse_and_validate("echo hello world");
assert!(result.is_ok(), "Safe command should validate successfully");
let report = result.unwrap();
assert_eq!(
report.safe_command.commands,
vec![vec![
"echo".to_string(),
"hello".to_string(),
"world".to_string()
]]
);
assert_eq!(report.security_analysis.threat_level, ThreatLevel::Safe);
assert!(
report.confidence_score > 0.8,
"Confidence should be high for safe commands"
);
assert_eq!(report.confidence_level, ConfidenceLevel::VeryHigh);
assert!(
report.applied_rewrites.is_empty(),
"No rewrites should be applied to safe commands"
);
}
#[test]
fn enhanced_parser_detects_forbidden_command() {
let parser = EnhancedBashParser::new().unwrap();
let result = parser.parse_and_validate("dangerous_command --malicious");
assert!(result.is_err(), "Forbidden command should be rejected");
match result.unwrap_err() {
BashParseError::ForbiddenCommand { command } => {
assert_eq!(command, "dangerous_command");
}
_ => panic!("Expected ForbiddenCommand error"),
}
}
#[test]
fn enhanced_parser_applies_rewrites() {
let parser = EnhancedBashParser::new().unwrap();
let result = parser.parse_and_validate("chmod 777 file.txt");
match result {
Ok(report) => {
if !report.applied_rewrites.is_empty() {
assert!(report.applied_rewrites[0].original_pattern.contains("777"));
assert!(report.applied_rewrites[0].replacement.contains("755"));
assert_eq!(report.applied_rewrites[0].reason, "Security enhancement");
}
}
Err(BashParseError::ForbiddenCommand { .. }) => {
}
Err(e) => panic!("Unexpected error: {:?}", e),
}
}
#[test]
fn enhanced_parser_security_analysis() {
let parser = EnhancedBashParser::new().unwrap();
let result = parser.parse_and_validate("find . -name '*.rs'");
match result {
Ok(report) => {
assert!(matches!(
report.security_analysis.threat_level,
ThreatLevel::Safe | ThreatLevel::Low
));
assert!(
report.confidence_score > 0.5,
"Should have reasonable confidence"
);
assert!(
report.validation_time_ms >= 0,
"Should have non-negative validation time"
);
}
Err(BashParseError::ForbiddenCommand { .. }) => {
}
Err(e) => panic!("Unexpected error: {:?}", e),
}
}
#[test]
fn enhanced_parser_handles_complex_safe_pipeline() {
let parser = EnhancedBashParser::new().unwrap();
let result = parser.parse_and_validate("ls -la | grep 'test' | sort");
match result {
Ok(report) => {
assert_eq!(
report.safe_command.commands.len(),
3,
"Should parse three commands in pipeline"
);
let commands: HashSet<String> = report
.safe_command
.commands
.iter()
.map(|cmd| cmd[0].clone())
.collect();
assert!(commands.contains("ls"));
assert!(commands.contains("grep"));
assert!(commands.contains("sort"));
}
Err(BashParseError::ForbiddenCommand { command }) => {
assert!(matches!(command.as_str(), "ls" | "grep" | "sort"));
}
Err(e) => panic!("Unexpected error: {:?}", e),
}
}
#[test]
fn command_validator_creation() {
let validator = CommandValidator::new();
let echo_analysis = validator.validate_command(&["echo".to_string(), "test".to_string()]);
assert!(echo_analysis.is_ok(), "Echo should be allowed");
let forbidden_analysis = validator.validate_command(&["malicious_cmd".to_string()]);
assert!(
forbidden_analysis.is_err(),
"Unknown command should be forbidden"
);
}
#[test]
fn command_rewriter_functionality() {
let rewriter = CommandRewriter::new();
let mut command = vec!["rm".to_string(), "-rf".to_string(), "file.txt".to_string()];
let rewrites = rewriter.rewrite_command(&mut command);
if !rewrites.is_empty() {
assert!(!rewrites[0].original_pattern.is_empty());
assert!(!rewrites[0].replacement.is_empty());
assert_eq!(rewrites[0].reason, "Security enhancement");
}
}
#[test]
fn sandbox_rules_blocking() {
let sandbox = SandboxRules::new();
let result = sandbox.apply_rules(&["cat".to_string(), "/etc/passwd".to_string()]);
match result {
Ok(()) => {
}
Err(BashParseError::UnsafeContext { reason }) => {
assert!(reason.contains("Blocked by rule") || reason.contains("Requires approval"));
}
Err(e) => panic!("Unexpected error type: {:?}", e),
}
}
#[test]
fn confidence_level_conversions() {
assert_eq!(ConfidenceLevel::from_score(0.1), ConfidenceLevel::Low);
assert_eq!(ConfidenceLevel::from_score(0.5), ConfidenceLevel::Medium);
assert_eq!(ConfidenceLevel::from_score(0.8), ConfidenceLevel::High);
assert_eq!(ConfidenceLevel::from_score(0.95), ConfidenceLevel::VeryHigh);
assert!(ConfidenceLevel::VeryHigh.as_score() > 0.9);
assert!(ConfidenceLevel::Low.as_score() < 0.5);
}
#[test]
fn threat_level_severity_ordering() {
let parser = EnhancedBashParser::new().unwrap();
assert!(
parser.threat_level_severity(ThreatLevel::Critical)
> parser.threat_level_severity(ThreatLevel::High)
);
assert!(
parser.threat_level_severity(ThreatLevel::High)
> parser.threat_level_severity(ThreatLevel::Medium)
);
assert!(
parser.threat_level_severity(ThreatLevel::Medium)
> parser.threat_level_severity(ThreatLevel::Low)
);
assert!(
parser.threat_level_severity(ThreatLevel::Low)
> parser.threat_level_severity(ThreatLevel::Safe)
);
}
#[test]
fn security_analysis_merging() {
let parser = EnhancedBashParser::new().unwrap();
let analyses = vec![
SecurityAnalysis {
threat_level: ThreatLevel::Low,
detected_patterns: vec!["pattern1".to_string()],
risk_factors: vec!["risk1".to_string()],
recommendations: vec!["rec1".to_string()],
},
SecurityAnalysis {
threat_level: ThreatLevel::Medium,
detected_patterns: vec!["pattern2".to_string(), "pattern1".to_string()], risk_factors: vec!["risk2".to_string()],
recommendations: vec!["rec2".to_string()],
},
];
let merged = parser.merge_security_analyses(analyses);
assert_eq!(merged.threat_level, ThreatLevel::Medium); assert_eq!(merged.detected_patterns.len(), 2); assert!(merged.detected_patterns.contains(&"pattern1".to_string()));
assert!(merged.detected_patterns.contains(&"pattern2".to_string()));
assert_eq!(merged.risk_factors.len(), 2);
assert_eq!(merged.recommendations.len(), 2);
}
#[test]
fn error_types_display_correctly() {
let parse_error = BashParseError::ParseError {
message: "test message".to_string(),
};
let error_str = format!("{}", parse_error);
assert!(error_str.contains("Failed to parse bash syntax"));
assert!(error_str.contains("test message"));
let danger_error = BashParseError::DangerousPattern {
pattern: "$()".to_string(),
command: "echo $(pwd)".to_string(),
};
let danger_str = format!("{}", danger_error);
assert!(danger_str.contains("Dangerous pattern detected"));
assert!(danger_str.contains("$()"));
}
#[test]
fn serialization_roundtrip() {
let report = SafeCommandReport {
safe_command: SafeCommand {
commands: vec![vec!["echo".to_string(), "test".to_string()]],
original_command: "echo test".to_string(),
final_command: "echo test".to_string(),
},
security_analysis: SecurityAnalysis {
threat_level: ThreatLevel::Safe,
detected_patterns: vec![],
risk_factors: vec![],
recommendations: vec![],
},
applied_rewrites: vec![],
confidence_score: 0.95,
confidence_level: ConfidenceLevel::VeryHigh,
validation_time_ms: 10,
};
let json = serde_json::to_string(&report).expect("Should serialize to JSON");
let deserialized: SafeCommandReport =
serde_json::from_str(&json).expect("Should deserialize from JSON");
assert_eq!(report, deserialized);
}
}