Skip to main content

driven/security/
capability_manifest.rs

1//! Capability Manifest
2//!
3//! Declarative security permissions for rule files.
4
5use std::collections::HashSet;
6
7/// Capability types
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9#[repr(u8)]
10pub enum Capability {
11    /// Read file system
12    FileRead = 0,
13    /// Write file system
14    FileWrite = 1,
15    /// Execute commands
16    Execute = 2,
17    /// Network access
18    Network = 3,
19    /// Environment variables
20    Environment = 4,
21    /// Modify settings
22    Settings = 5,
23    /// Access secrets/credentials
24    Secrets = 6,
25    /// Create processes
26    Process = 7,
27    /// Inter-process communication
28    Ipc = 8,
29    /// Full system access (dangerous)
30    System = 255,
31}
32
33impl Capability {
34    /// Parse from string
35    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    /// Convert to string
52    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    /// Check if capability implies another
68    pub fn implies(&self, other: &Capability) -> bool {
69        // System implies everything
70        if *self == Self::System {
71            return true;
72        }
73
74        // FileWrite implies FileRead
75        if *self == Self::FileWrite && *other == Self::FileRead {
76            return true;
77        }
78
79        // Process implies Execute
80        if *self == Self::Process && *other == Self::Execute {
81            return true;
82        }
83
84        *self == *other
85    }
86}
87
88/// Capability manifest for a rule file
89#[derive(Debug, Clone, Default)]
90pub struct CapabilityManifest {
91    /// Required capabilities
92    required: HashSet<Capability>,
93    /// Optional capabilities (requested but not required)
94    optional: HashSet<Capability>,
95    /// Denied capabilities (explicitly blocked)
96    denied: HashSet<Capability>,
97    /// Resource restrictions (e.g., allowed paths)
98    restrictions: Vec<CapabilityRestriction>,
99}
100
101/// Restriction on a capability
102#[derive(Debug, Clone)]
103pub struct CapabilityRestriction {
104    /// Capability this restricts
105    pub capability: Capability,
106    /// Restriction type
107    pub restriction: RestrictionType,
108    /// Allowed values (paths, domains, etc.)
109    pub allowed: Vec<String>,
110}
111
112/// Types of restrictions
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub enum RestrictionType {
115    /// Restrict to specific paths
116    Path,
117    /// Restrict to specific domains
118    Domain,
119    /// Restrict to specific commands
120    Command,
121    /// Restrict to specific environment variables
122    EnvVar,
123}
124
125impl CapabilityManifest {
126    /// Create an empty manifest
127    pub fn new() -> Self {
128        Self::default()
129    }
130
131    /// Create a manifest with default safe capabilities
132    pub fn safe() -> Self {
133        let mut manifest = Self::new();
134        manifest.require(Capability::FileRead);
135        manifest
136    }
137
138    /// Create a manifest with all capabilities
139    pub fn unrestricted() -> Self {
140        let mut manifest = Self::new();
141        manifest.require(Capability::System);
142        manifest
143    }
144
145    /// Require a capability
146    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    /// Request an optional capability
153    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    /// Deny a capability
160    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    /// Add a restriction
167    pub fn restrict(&mut self, restriction: CapabilityRestriction) {
168        self.restrictions.push(restriction);
169    }
170
171    /// Check if capability is required
172    pub fn requires(&self, cap: Capability) -> bool {
173        self.required.iter().any(|r| r.implies(&cap))
174    }
175
176    /// Check if capability is allowed
177    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    /// Check if capability is denied
186    pub fn denies(&self, cap: Capability) -> bool {
187        self.denied.iter().any(|d| d.implies(&cap))
188    }
189
190    /// Get all required capabilities
191    pub fn required_capabilities(&self) -> &HashSet<Capability> {
192        &self.required
193    }
194
195    /// Get all optional capabilities
196    pub fn optional_capabilities(&self) -> &HashSet<Capability> {
197        &self.optional
198    }
199
200    /// Get restrictions for a capability
201    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    /// Check if a path is allowed for file operations
211    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 no path restrictions, allow all paths
222        if restrictions.is_empty() {
223            return true;
224        }
225
226        // Check if path matches any allowed pattern
227        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    /// Serialize to bytes
239    pub fn to_bytes(&self) -> Vec<u8> {
240        let mut output = Vec::new();
241
242        // Required count + capabilities
243        output.push(self.required.len() as u8);
244        for cap in &self.required {
245            output.push(*cap as u8);
246        }
247
248        // Optional count + capabilities
249        output.push(self.optional.len() as u8);
250        for cap in &self.optional {
251            output.push(*cap as u8);
252        }
253
254        // Denied count + capabilities
255        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}