Skip to main content

driven/security/
integrity_guard.rs

1//! Integrity Guard
2//!
3//! Continuous integrity verification for rule files.
4
5use crate::Result;
6use crate::binary::checksum::{Blake3Checksum, compute_blake3};
7use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9
10/// Integrity status
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum IntegrityStatus {
13    /// File has not been verified
14    Unknown,
15    /// File integrity is verified
16    Verified,
17    /// File has been modified
18    Modified,
19    /// File signature is invalid
20    InvalidSignature,
21    /// File is missing
22    Missing,
23}
24
25/// Integrity record for a file
26#[derive(Debug, Clone)]
27pub struct IntegrityRecord {
28    /// File path
29    pub path: PathBuf,
30    /// Content checksum
31    pub checksum: Blake3Checksum,
32    /// File size
33    pub size: u64,
34    /// Last modified time
35    pub modified: std::time::SystemTime,
36    /// Status
37    pub status: IntegrityStatus,
38}
39
40/// Integrity guard for monitoring file integrity
41pub struct IntegrityGuard {
42    /// Tracked files
43    records: HashMap<PathBuf, IntegrityRecord>,
44    /// Verification callback
45    on_violation: Option<Box<dyn Fn(&IntegrityRecord) + Send + Sync>>,
46}
47
48impl std::fmt::Debug for IntegrityGuard {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        f.debug_struct("IntegrityGuard")
51            .field("records", &self.records)
52            .field("on_violation", &self.on_violation.is_some())
53            .finish()
54    }
55}
56
57impl IntegrityGuard {
58    /// Create a new integrity guard
59    pub fn new() -> Self {
60        Self {
61            records: HashMap::new(),
62            on_violation: None,
63        }
64    }
65
66    /// Set violation callback
67    pub fn on_violation<F>(mut self, callback: F) -> Self
68    where
69        F: Fn(&IntegrityRecord) + Send + Sync + 'static,
70    {
71        self.on_violation = Some(Box::new(callback));
72        self
73    }
74
75    /// Add a file to track
76    pub fn track(&mut self, path: &Path) -> Result<IntegrityStatus> {
77        let metadata = match std::fs::metadata(path) {
78            Ok(m) => m,
79            Err(_) => {
80                self.records.insert(
81                    path.to_path_buf(),
82                    IntegrityRecord {
83                        path: path.to_path_buf(),
84                        checksum: [0; 16],
85                        size: 0,
86                        modified: std::time::UNIX_EPOCH,
87                        status: IntegrityStatus::Missing,
88                    },
89                );
90                return Ok(IntegrityStatus::Missing);
91            }
92        };
93
94        let content = std::fs::read(path)?;
95        let checksum = compute_blake3(&content);
96
97        let record = IntegrityRecord {
98            path: path.to_path_buf(),
99            checksum,
100            size: metadata.len(),
101            modified: metadata.modified().unwrap_or(std::time::UNIX_EPOCH),
102            status: IntegrityStatus::Verified,
103        };
104
105        self.records.insert(path.to_path_buf(), record);
106        Ok(IntegrityStatus::Verified)
107    }
108
109    /// Verify a file's integrity
110    pub fn verify(&mut self, path: &Path) -> Result<IntegrityStatus> {
111        let record = match self.records.get(path) {
112            Some(r) => r.clone(),
113            None => {
114                // Not tracked, track it now
115                return self.track(path);
116            }
117        };
118
119        // Check if file exists
120        let metadata = match std::fs::metadata(path) {
121            Ok(m) => m,
122            Err(_) => {
123                let mut record = record;
124                record.status = IntegrityStatus::Missing;
125                if let Some(cb) = &self.on_violation {
126                    cb(&record);
127                }
128                self.records.insert(path.to_path_buf(), record);
129                return Ok(IntegrityStatus::Missing);
130            }
131        };
132
133        // Quick check: size changed?
134        if metadata.len() != record.size {
135            let mut record = record;
136            record.status = IntegrityStatus::Modified;
137            if let Some(cb) = &self.on_violation {
138                cb(&record);
139            }
140            self.records.insert(path.to_path_buf(), record);
141            return Ok(IntegrityStatus::Modified);
142        }
143
144        // Full check: content changed?
145        let content = std::fs::read(path)?;
146        let current_checksum = compute_blake3(&content);
147
148        if current_checksum != record.checksum {
149            let mut record = record;
150            record.status = IntegrityStatus::Modified;
151            if let Some(cb) = &self.on_violation {
152                cb(&record);
153            }
154            self.records.insert(path.to_path_buf(), record);
155            return Ok(IntegrityStatus::Modified);
156        }
157
158        Ok(IntegrityStatus::Verified)
159    }
160
161    /// Verify all tracked files
162    pub fn verify_all(&mut self) -> Result<Vec<(PathBuf, IntegrityStatus)>> {
163        let paths: Vec<_> = self.records.keys().cloned().collect();
164        let mut results = Vec::with_capacity(paths.len());
165
166        for path in paths {
167            let status = self.verify(&path)?;
168            results.push((path, status));
169        }
170
171        Ok(results)
172    }
173
174    /// Get status of a file
175    pub fn status(&self, path: &Path) -> IntegrityStatus {
176        self.records
177            .get(path)
178            .map(|r| r.status)
179            .unwrap_or(IntegrityStatus::Unknown)
180    }
181
182    /// Get all records
183    pub fn records(&self) -> &HashMap<PathBuf, IntegrityRecord> {
184        &self.records
185    }
186
187    /// Remove a file from tracking
188    pub fn untrack(&mut self, path: &Path) {
189        self.records.remove(path);
190    }
191
192    /// Clear all tracking
193    pub fn clear(&mut self) {
194        self.records.clear();
195    }
196
197    /// Update checksum after authorized modification
198    pub fn update(&mut self, path: &Path) -> Result<()> {
199        self.track(path)?;
200        Ok(())
201    }
202
203    /// Get number of tracked files
204    pub fn len(&self) -> usize {
205        self.records.len()
206    }
207
208    /// Check if any files are tracked
209    pub fn is_empty(&self) -> bool {
210        self.records.is_empty()
211    }
212
213    /// Count files by status
214    pub fn count_by_status(&self) -> HashMap<IntegrityStatus, usize> {
215        let mut counts = HashMap::new();
216        for record in self.records.values() {
217            *counts.entry(record.status).or_insert(0) += 1;
218        }
219        counts
220    }
221}
222
223impl Default for IntegrityGuard {
224    fn default() -> Self {
225        Self::new()
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    #[test]
234    fn test_track_and_verify() {
235        let temp_dir = std::env::temp_dir().join("integrity_test");
236        std::fs::create_dir_all(&temp_dir).unwrap();
237        let test_file = temp_dir.join("test.txt");
238
239        std::fs::write(&test_file, b"test content").unwrap();
240
241        let mut guard = IntegrityGuard::new();
242        let status = guard.track(&test_file).unwrap();
243        assert_eq!(status, IntegrityStatus::Verified);
244
245        // Verify unchanged
246        let status = guard.verify(&test_file).unwrap();
247        assert_eq!(status, IntegrityStatus::Verified);
248
249        // Modify file
250        std::fs::write(&test_file, b"modified content").unwrap();
251
252        // Verify should detect modification
253        let status = guard.verify(&test_file).unwrap();
254        assert_eq!(status, IntegrityStatus::Modified);
255
256        // Cleanup
257        std::fs::remove_dir_all(&temp_dir).ok();
258    }
259
260    #[test]
261    fn test_missing_file() {
262        let mut guard = IntegrityGuard::new();
263        let status = guard.track(Path::new("/nonexistent/file.txt")).unwrap();
264        assert_eq!(status, IntegrityStatus::Missing);
265    }
266}