driven/security/
capability_manifest.rs1use std::collections::HashSet;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9#[repr(u8)]
10pub enum Capability {
11 FileRead = 0,
13 FileWrite = 1,
15 Execute = 2,
17 Network = 3,
19 Environment = 4,
21 Settings = 5,
23 Secrets = 6,
25 Process = 7,
27 Ipc = 8,
29 System = 255,
31}
32
33impl Capability {
34 pub fn from_str(s: &str) -> Option<Self> {
36 match s.to_lowercase().as_str() {
37 "file-read" | "fs:read" => Some(Self::FileRead),
38 "file-write" | "fs:write" => Some(Self::FileWrite),
39 "execute" | "exec" => Some(Self::Execute),
40 "network" | "net" => Some(Self::Network),
41 "environment" | "env" => Some(Self::Environment),
42 "settings" | "config" => Some(Self::Settings),
43 "secrets" | "credentials" => Some(Self::Secrets),
44 "process" | "spawn" => Some(Self::Process),
45 "ipc" => Some(Self::Ipc),
46 "system" | "all" => Some(Self::System),
47 _ => None,
48 }
49 }
50
51 pub fn as_str(&self) -> &'static str {
53 match self {
54 Self::FileRead => "fs:read",
55 Self::FileWrite => "fs:write",
56 Self::Execute => "execute",
57 Self::Network => "network",
58 Self::Environment => "environment",
59 Self::Settings => "settings",
60 Self::Secrets => "secrets",
61 Self::Process => "process",
62 Self::Ipc => "ipc",
63 Self::System => "system",
64 }
65 }
66
67 pub fn implies(&self, other: &Capability) -> bool {
69 if *self == Self::System {
71 return true;
72 }
73
74 if *self == Self::FileWrite && *other == Self::FileRead {
76 return true;
77 }
78
79 if *self == Self::Process && *other == Self::Execute {
81 return true;
82 }
83
84 *self == *other
85 }
86}
87
88#[derive(Debug, Clone, Default)]
90pub struct CapabilityManifest {
91 required: HashSet<Capability>,
93 optional: HashSet<Capability>,
95 denied: HashSet<Capability>,
97 restrictions: Vec<CapabilityRestriction>,
99}
100
101#[derive(Debug, Clone)]
103pub struct CapabilityRestriction {
104 pub capability: Capability,
106 pub restriction: RestrictionType,
108 pub allowed: Vec<String>,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq)]
114pub enum RestrictionType {
115 Path,
117 Domain,
119 Command,
121 EnvVar,
123}
124
125impl CapabilityManifest {
126 pub fn new() -> Self {
128 Self::default()
129 }
130
131 pub fn safe() -> Self {
133 let mut manifest = Self::new();
134 manifest.require(Capability::FileRead);
135 manifest
136 }
137
138 pub fn unrestricted() -> Self {
140 let mut manifest = Self::new();
141 manifest.require(Capability::System);
142 manifest
143 }
144
145 pub fn require(&mut self, cap: Capability) {
147 self.required.insert(cap);
148 self.optional.remove(&cap);
149 self.denied.remove(&cap);
150 }
151
152 pub fn request(&mut self, cap: Capability) {
154 if !self.required.contains(&cap) && !self.denied.contains(&cap) {
155 self.optional.insert(cap);
156 }
157 }
158
159 pub fn deny(&mut self, cap: Capability) {
161 self.required.remove(&cap);
162 self.optional.remove(&cap);
163 self.denied.insert(cap);
164 }
165
166 pub fn restrict(&mut self, restriction: CapabilityRestriction) {
168 self.restrictions.push(restriction);
169 }
170
171 pub fn requires(&self, cap: Capability) -> bool {
173 self.required.iter().any(|r| r.implies(&cap))
174 }
175
176 pub fn allows(&self, cap: Capability) -> bool {
178 if self.denied.iter().any(|d| d.implies(&cap)) {
179 return false;
180 }
181 self.required.iter().any(|r| r.implies(&cap))
182 || self.optional.iter().any(|o| o.implies(&cap))
183 }
184
185 pub fn denies(&self, cap: Capability) -> bool {
187 self.denied.iter().any(|d| d.implies(&cap))
188 }
189
190 pub fn required_capabilities(&self) -> &HashSet<Capability> {
192 &self.required
193 }
194
195 pub fn optional_capabilities(&self) -> &HashSet<Capability> {
197 &self.optional
198 }
199
200 pub fn restrictions_for(
202 &self,
203 cap: Capability,
204 ) -> impl Iterator<Item = &CapabilityRestriction> {
205 self.restrictions
206 .iter()
207 .filter(move |r| r.capability == cap)
208 }
209
210 pub fn is_path_allowed(&self, path: &str, cap: Capability) -> bool {
212 if !self.allows(cap) {
213 return false;
214 }
215
216 let restrictions: Vec<_> = self
217 .restrictions_for(cap)
218 .filter(|r| r.restriction == RestrictionType::Path)
219 .collect();
220
221 if restrictions.is_empty() {
223 return true;
224 }
225
226 restrictions.iter().any(|r| {
228 r.allowed.iter().any(|allowed| {
229 path.starts_with(allowed)
230 || globset::Glob::new(allowed)
231 .ok()
232 .and_then(|g| g.compile_matcher().is_match(path).then_some(()))
233 .is_some()
234 })
235 })
236 }
237
238 pub fn to_bytes(&self) -> Vec<u8> {
240 let mut output = Vec::new();
241
242 output.push(self.required.len() as u8);
244 for cap in &self.required {
245 output.push(*cap as u8);
246 }
247
248 output.push(self.optional.len() as u8);
250 for cap in &self.optional {
251 output.push(*cap as u8);
252 }
253
254 output.push(self.denied.len() as u8);
256 for cap in &self.denied {
257 output.push(*cap as u8);
258 }
259
260 output
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 #[test]
269 fn test_capability_implies() {
270 assert!(Capability::System.implies(&Capability::FileRead));
271 assert!(Capability::FileWrite.implies(&Capability::FileRead));
272 assert!(!Capability::FileRead.implies(&Capability::FileWrite));
273 }
274
275 #[test]
276 fn test_manifest_require() {
277 let mut manifest = CapabilityManifest::new();
278 manifest.require(Capability::FileRead);
279
280 assert!(manifest.requires(Capability::FileRead));
281 assert!(manifest.allows(Capability::FileRead));
282 assert!(!manifest.denies(Capability::FileRead));
283 }
284
285 #[test]
286 fn test_manifest_deny() {
287 let mut manifest = CapabilityManifest::new();
288 manifest.deny(Capability::Network);
289
290 assert!(manifest.denies(Capability::Network));
291 assert!(!manifest.allows(Capability::Network));
292 }
293
294 #[test]
295 fn test_path_restrictions() {
296 let mut manifest = CapabilityManifest::new();
297 manifest.require(Capability::FileRead);
298 manifest.restrict(CapabilityRestriction {
299 capability: Capability::FileRead,
300 restriction: RestrictionType::Path,
301 allowed: vec!["/home/user/project".to_string()],
302 });
303
304 assert!(manifest.is_path_allowed("/home/user/project/src/main.rs", Capability::FileRead));
305 assert!(!manifest.is_path_allowed("/etc/passwd", Capability::FileRead));
306 }
307}