Skip to main content

cc_toolgate/commands/
simple.rs

1//! Data-driven command spec for flat allow/ask/deny command lists.
2
3use crate::commands::CommandSpec;
4use crate::eval::{CommandContext, Decision, RuleMatch};
5
6/// A data-driven command spec for flat allow/ask commands.
7///
8/// For allow commands: returns Allow unless output redirection is detected (→ Ask).
9/// `--version` on any allowed command is also allowed.
10/// For ask commands: always returns Ask.
11pub struct SimpleCommandSpec {
12    /// The baseline decision for this command (Allow, Ask, or Deny).
13    decision: Decision,
14}
15
16impl SimpleCommandSpec {
17    /// Create a new spec with the given baseline decision.
18    pub fn new(decision: Decision) -> Self {
19        Self { decision }
20    }
21}
22
23impl CommandSpec for SimpleCommandSpec {
24    fn evaluate(&self, ctx: &CommandContext) -> RuleMatch {
25        match self.decision {
26            Decision::Allow => {
27                // Check for --version on any allowed command
28                if ctx.words.len() <= 3 && ctx.has_flag("--version") {
29                    return RuleMatch {
30                        decision: Decision::Allow,
31                        reason: format!("{} --version", ctx.base_command),
32                    };
33                }
34                // Redirection escalates ALLOW → ASK
35                if let Some(ref r) = ctx.redirection {
36                    return RuleMatch {
37                        decision: Decision::Ask,
38                        reason: format!("{} with {}", ctx.base_command, r.description),
39                    };
40                }
41                RuleMatch {
42                    decision: Decision::Allow,
43                    reason: format!("allowed: {}", ctx.base_command),
44                }
45            }
46            Decision::Ask => RuleMatch {
47                decision: Decision::Ask,
48                reason: format!("{} requires confirmation", ctx.base_command),
49            },
50            Decision::Deny => RuleMatch {
51                decision: Decision::Deny,
52                reason: format!("blocked command: {}", ctx.base_command),
53            },
54        }
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[test]
63    fn allow_simple() {
64        let spec = SimpleCommandSpec::new(Decision::Allow);
65        let ctx = CommandContext::from_command("ls -la");
66        assert_eq!(spec.evaluate(&ctx).decision, Decision::Allow);
67    }
68
69    #[test]
70    fn allow_with_redir() {
71        let spec = SimpleCommandSpec::new(Decision::Allow);
72        let ctx = CommandContext::from_command("ls > file.txt");
73        assert_eq!(spec.evaluate(&ctx).decision, Decision::Ask);
74    }
75
76    #[test]
77    fn ask_simple() {
78        let spec = SimpleCommandSpec::new(Decision::Ask);
79        let ctx = CommandContext::from_command("rm -rf /tmp");
80        assert_eq!(spec.evaluate(&ctx).decision, Decision::Ask);
81    }
82
83    #[test]
84    fn deny_simple() {
85        let spec = SimpleCommandSpec::new(Decision::Deny);
86        let ctx = CommandContext::from_command("shred /dev/sda");
87        assert_eq!(spec.evaluate(&ctx).decision, Decision::Deny);
88    }
89}