1use serde::{Deserialize, Serialize};
4use std::default::Default;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
8#[serde(rename_all = "lowercase")]
9pub enum Decision {
10 Allow,
12
13 #[default]
15 Prompt,
16
17 Forbidden,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct PrefixRule {
24 pub pattern: Vec<String>,
26
27 pub decision: Decision,
29}
30
31impl PrefixRule {
32 pub fn new(pattern: Vec<String>, decision: Decision) -> Self {
34 Self { pattern, decision }
35 }
36
37 pub fn matches(&self, command: &[String]) -> bool {
39 if command.len() < self.pattern.len() {
40 return false;
41 }
42 self.pattern
43 .iter()
44 .zip(command.iter())
45 .all(|(pattern, cmd)| pattern == cmd)
46 }
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
51pub enum RuleMatch {
52 PrefixRuleMatch {
54 rule: PrefixRule,
55 decision: Decision,
56 },
57
58 HeuristicsRuleMatch { decision: Decision },
60}
61
62impl RuleMatch {
63 pub fn decision(&self) -> Decision {
65 match self {
66 Self::PrefixRuleMatch { decision, .. } => *decision,
67 Self::HeuristicsRuleMatch { decision } => *decision,
68 }
69 }
70
71 pub fn is_policy_match(&self) -> bool {
73 matches!(self, Self::PrefixRuleMatch { .. })
74 }
75}
76
77#[derive(Debug, Clone)]
79pub struct PolicyEvaluation {
80 pub decision: Decision,
82
83 pub matched_rules: Vec<RuleMatch>,
85}
86
87#[derive(Debug, Clone, Default)]
89pub struct Policy {
90 prefix_rules: Vec<PrefixRule>,
92}
93
94impl Policy {
95 pub fn empty() -> Self {
97 Self {
98 prefix_rules: Vec::new(),
99 }
100 }
101
102 pub fn add_prefix_rule(
104 &mut self,
105 pattern: &[String],
106 decision: Decision,
107 ) -> anyhow::Result<()> {
108 self.prefix_rules
109 .push(PrefixRule::new(pattern.to_vec(), decision));
110 Ok(())
111 }
112
113 pub fn check(&self, command: &[String]) -> RuleMatch {
115 for rule in &self.prefix_rules {
116 if rule.matches(command) {
117 return RuleMatch::PrefixRuleMatch {
118 rule: rule.clone(),
119 decision: rule.decision,
120 };
121 }
122 }
123
124 RuleMatch::HeuristicsRuleMatch {
126 decision: Decision::Prompt,
127 }
128 }
129
130 pub fn check_multiple<'a, I, F>(&self, commands: I, heuristics_fallback: &F) -> PolicyEvaluation
132 where
133 I: Iterator<Item = &'a Vec<String>>,
134 F: Fn(&[String]) -> Decision,
135 {
136 let mut matched_rules = Vec::new();
137 let mut overall_decision = Decision::Allow;
138
139 for command in commands {
140 let rule_match = self.check(command);
141
142 let decision = match &rule_match {
144 RuleMatch::PrefixRuleMatch { decision, .. } => *decision,
145 RuleMatch::HeuristicsRuleMatch { .. } => heuristics_fallback(command),
146 };
147
148 overall_decision = match (overall_decision, decision) {
150 (Decision::Forbidden, _) | (_, Decision::Forbidden) => Decision::Forbidden,
151 (Decision::Prompt, _) | (_, Decision::Prompt) => Decision::Prompt,
152 (Decision::Allow, Decision::Allow) => Decision::Allow,
153 };
154
155 matched_rules.push(rule_match);
156 }
157
158 PolicyEvaluation {
159 decision: overall_decision,
160 matched_rules,
161 }
162 }
163
164 pub fn prefix_rules(&self) -> &[PrefixRule] {
166 &self.prefix_rules
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn test_prefix_rule_matching() {
176 let rule = PrefixRule::new(
177 vec!["cargo".to_string(), "build".to_string()],
178 Decision::Allow,
179 );
180
181 assert!(rule.matches(&["cargo".to_string(), "build".to_string()]));
182 assert!(rule.matches(&[
183 "cargo".to_string(),
184 "build".to_string(),
185 "--release".to_string()
186 ]));
187 assert!(!rule.matches(&["cargo".to_string(), "test".to_string()]));
188 assert!(!rule.matches(&["cargo".to_string()]));
189 }
190
191 #[test]
192 fn test_policy_check() {
193 let mut policy = Policy::empty();
194 policy
195 .add_prefix_rule(&["cargo".to_string(), "build".to_string()], Decision::Allow)
196 .unwrap();
197 policy
198 .add_prefix_rule(&["rm".to_string()], Decision::Forbidden)
199 .unwrap();
200
201 let allow = policy.check(&["cargo".to_string(), "build".to_string()]);
202 assert_eq!(allow.decision(), Decision::Allow);
203 assert!(allow.is_policy_match());
204
205 let forbidden = policy.check(&["rm".to_string(), "-rf".to_string()]);
206 assert_eq!(forbidden.decision(), Decision::Forbidden);
207
208 let heuristics = policy.check(&["unknown".to_string()]);
209 assert!(!heuristics.is_policy_match());
210 }
211
212 #[test]
213 fn test_policy_evaluation() {
214 let mut policy = Policy::empty();
215 policy
216 .add_prefix_rule(&["echo".to_string()], Decision::Allow)
217 .unwrap();
218 policy
219 .add_prefix_rule(&["rm".to_string()], Decision::Forbidden)
220 .unwrap();
221
222 let commands = [
223 vec!["echo".to_string(), "hello".to_string()],
224 vec!["rm".to_string(), "-rf".to_string()],
225 ];
226
227 let evaluation = policy.check_multiple(commands.iter(), &|_| Decision::Prompt);
228
229 assert_eq!(evaluation.decision, Decision::Forbidden);
231 }
232}