sh_layer2/permission/
policy.rs1use serde::{Deserialize, Serialize};
6use std::collections::HashSet;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
10pub enum SecurityLevel {
11 Trusted,
13 #[default]
15 Standard,
16 Strict,
18 Paranoid,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct PermissionRule {
25 pub pattern: String,
27 pub allow: bool,
29 pub description: Option<String>,
31}
32
33impl PermissionRule {
34 pub fn allow(pattern: impl Into<String>) -> Self {
36 Self {
37 pattern: pattern.into(),
38 allow: true,
39 description: None,
40 }
41 }
42
43 pub fn deny(pattern: impl Into<String>) -> Self {
45 Self {
46 pattern: pattern.into(),
47 allow: false,
48 description: None,
49 }
50 }
51
52 pub fn with_description(mut self, description: impl Into<String>) -> Self {
54 self.description = Some(description.into());
55 self
56 }
57
58 pub fn matches(&self, value: &str) -> bool {
60 let pattern_parts: Vec<&str> = self.pattern.split('*').collect();
62 if pattern_parts.len() == 1 {
63 return value == self.pattern;
64 }
65
66 if !value.starts_with(pattern_parts[0]) {
68 return false;
69 }
70 if !value.ends_with(pattern_parts.last().unwrap()) {
71 return false;
72 }
73
74 let mut search_start = pattern_parts[0].len();
76 for part in pattern_parts.iter().skip(1).take(pattern_parts.len() - 2) {
77 if let Some(pos) = value[search_start..].find(part) {
78 search_start += pos + part.len();
79 } else {
80 return false;
81 }
82 }
83
84 true
85 }
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct PermissionPolicy {
91 pub level: SecurityLevel,
93 pub allow_rules: Vec<PermissionRule>,
95 pub deny_rules: Vec<PermissionRule>,
97 pub trusted_categories: HashSet<String>,
99 pub blocked_categories: HashSet<String>,
101 pub trusted_paths: HashSet<String>,
103 pub blocked_paths: HashSet<String>,
105 pub trusted_urls: HashSet<String>,
107 pub blocked_urls: HashSet<String>,
109 pub trusted_commands: HashSet<String>,
111 pub blocked_commands: HashSet<String>,
113 pub enable_cache: bool,
115 pub cache_expire_seconds: u64,
117 pub audit_enabled: bool,
119 pub max_audit_entries: usize,
121}
122
123impl Default for PermissionPolicy {
124 fn default() -> Self {
125 Self {
126 level: SecurityLevel::Standard,
127 allow_rules: Vec::new(),
128 deny_rules: Vec::new(),
129 trusted_categories: HashSet::new(),
130 blocked_categories: HashSet::new(),
131 trusted_paths: HashSet::new(),
132 blocked_paths: Self::default_blocked_paths(),
133 trusted_urls: HashSet::new(),
134 blocked_urls: HashSet::new(),
135 trusted_commands: HashSet::new(),
136 blocked_commands: Self::default_blocked_commands(),
137 enable_cache: true,
138 cache_expire_seconds: 3600, audit_enabled: true,
140 max_audit_entries: 10000,
141 }
142 }
143}
144
145impl PermissionPolicy {
146 pub fn trusted() -> Self {
148 Self {
149 level: SecurityLevel::Trusted,
150 ..Self::default()
151 }
152 }
153
154 pub fn strict() -> Self {
156 Self {
157 level: SecurityLevel::Strict,
158 ..Self::default()
159 }
160 }
161
162 pub fn paranoid() -> Self {
164 Self {
165 level: SecurityLevel::Paranoid,
166 audit_enabled: true,
167 ..Self::strict()
168 }
169 }
170
171 fn default_blocked_paths() -> HashSet<String> {
173 let mut paths = HashSet::new();
174 paths.insert(".env".to_string());
176 paths.insert(".env.local".to_string());
177 paths.insert(".env.*.local".to_string());
178 paths.insert("**/credentials.json".to_string());
180 paths.insert("**/secrets.json".to_string());
181 paths.insert("**/api_keys.json".to_string());
182 paths.insert("~/.ssh/id_rsa".to_string());
184 paths.insert("~/.ssh/id_ed25519".to_string());
185 paths.insert("/etc/shadow".to_string());
187 paths.insert("/etc/passwd".to_string());
188 paths
189 }
190
191 fn default_blocked_commands() -> HashSet<String> {
193 let mut commands = HashSet::new();
194 commands.insert("rm -rf /".to_string());
196 commands.insert("rm -rf ~".to_string());
197 commands.insert("mkfs".to_string());
198 commands.insert("dd if=/dev/zero".to_string());
199 commands.insert(":(){ :|:& };:".to_string()); commands.insert("chmod 777".to_string());
201 commands
202 }
203
204 pub fn add_trusted_path(mut self, path: impl Into<String>) -> Self {
206 self.trusted_paths.insert(path.into());
207 self
208 }
209
210 pub fn add_blocked_path(mut self, path: impl Into<String>) -> Self {
212 self.blocked_paths.insert(path.into());
213 self
214 }
215
216 pub fn add_trusted_url(mut self, url: impl Into<String>) -> Self {
218 self.trusted_urls.insert(url.into());
219 self
220 }
221
222 pub fn add_blocked_url(mut self, url: impl Into<String>) -> Self {
224 self.blocked_urls.insert(url.into());
225 self
226 }
227
228 pub fn add_trusted_command(mut self, command: impl Into<String>) -> Self {
230 self.trusted_commands.insert(command.into());
231 self
232 }
233
234 pub fn add_blocked_command(mut self, command: impl Into<String>) -> Self {
236 self.blocked_commands.insert(command.into());
237 self
238 }
239
240 pub fn is_path_trusted(&self, path: &str) -> bool {
242 self.trusted_paths
243 .iter()
244 .any(|p| path.starts_with(p) || PermissionRule::allow(p.clone()).matches(path))
245 }
246
247 pub fn is_path_blocked(&self, path: &str) -> bool {
249 self.blocked_paths
250 .iter()
251 .any(|p| path.starts_with(p) || PermissionRule::deny(p.clone()).matches(path))
252 }
253
254 pub fn should_auto_approve(&self, action_category: &str) -> bool {
256 match self.level {
257 SecurityLevel::Trusted => true,
258 SecurityLevel::Standard => self.trusted_categories.contains(action_category),
259 SecurityLevel::Strict | SecurityLevel::Paranoid => false,
260 }
261 }
262
263 pub fn is_category_blocked(&self, category: &str) -> bool {
265 self.blocked_categories.contains(category)
266 }
267
268 pub fn load_from_file(path: &std::path::Path) -> anyhow::Result<Self> {
270 let content = std::fs::read_to_string(path)?;
271 let policy: Self = toml::from_str(&content)?;
272 Ok(policy)
273 }
274
275 pub fn save_to_file(&self, path: &std::path::Path) -> anyhow::Result<()> {
277 let content = toml::to_string_pretty(self)?;
278 std::fs::write(path, content)?;
279 Ok(())
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[test]
288 fn test_permission_rule_matching() {
289 let rule = PermissionRule::allow("/home/user/*.txt");
290 assert!(rule.matches("/home/user/test.txt"));
291 assert!(rule.matches("/home/user/another.txt"));
292 assert!(!rule.matches("/home/other/test.txt"));
293 }
294
295 #[test]
296 fn test_default_blocked_paths() {
297 let policy = PermissionPolicy::default();
298 assert!(policy.is_path_blocked(".env"));
299 assert!(policy.is_path_blocked("/etc/shadow"));
300 }
301
302 #[test]
303 fn test_security_level_auto_approve() {
304 let trusted_policy = PermissionPolicy::trusted();
305 assert!(trusted_policy.should_auto_approve("file_read"));
306
307 let strict_policy = PermissionPolicy::strict();
308 assert!(!strict_policy.should_auto_approve("file_read"));
309 }
310}