driven/security/
integrity_guard.rs1use crate::Result;
6use crate::binary::checksum::{Blake3Checksum, compute_blake3};
7use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum IntegrityStatus {
13 Unknown,
15 Verified,
17 Modified,
19 InvalidSignature,
21 Missing,
23}
24
25#[derive(Debug, Clone)]
27pub struct IntegrityRecord {
28 pub path: PathBuf,
30 pub checksum: Blake3Checksum,
32 pub size: u64,
34 pub modified: std::time::SystemTime,
36 pub status: IntegrityStatus,
38}
39
40pub struct IntegrityGuard {
42 records: HashMap<PathBuf, IntegrityRecord>,
44 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 pub fn new() -> Self {
60 Self {
61 records: HashMap::new(),
62 on_violation: None,
63 }
64 }
65
66 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 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 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 return self.track(path);
116 }
117 };
118
119 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 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 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 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 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 pub fn records(&self) -> &HashMap<PathBuf, IntegrityRecord> {
184 &self.records
185 }
186
187 pub fn untrack(&mut self, path: &Path) {
189 self.records.remove(path);
190 }
191
192 pub fn clear(&mut self) {
194 self.records.clear();
195 }
196
197 pub fn update(&mut self, path: &Path) -> Result<()> {
199 self.track(path)?;
200 Ok(())
201 }
202
203 pub fn len(&self) -> usize {
205 self.records.len()
206 }
207
208 pub fn is_empty(&self) -> bool {
210 self.records.is_empty()
211 }
212
213 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 let status = guard.verify(&test_file).unwrap();
247 assert_eq!(status, IntegrityStatus::Verified);
248
249 std::fs::write(&test_file, b"modified content").unwrap();
251
252 let status = guard.verify(&test_file).unwrap();
254 assert_eq!(status, IntegrityStatus::Modified);
255
256 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}