agentsec_core/scan/
snapshot.rs1use crate::Paths;
17use crate::error::{Error, Result};
18use crate::scan::ScanReport;
19use std::fs;
20use std::path::{Path, PathBuf};
21
22pub fn save(paths: &Paths, report: &ScanReport) -> Result<PathBuf> {
30 let dir = paths.snapshots();
31 fs::create_dir_all(&dir)?;
32 let stamp = report.scanned_at.format("%Y-%m-%d-%H%M%S").to_string();
33 let path = dir.join(format!("{stamp}.json"));
34 let body = serde_json::to_string_pretty(report)?;
35 fs::write(&path, body)?;
36 Ok(path)
37}
38
39pub fn load_latest(paths: &Paths) -> Result<Option<ScanReport>> {
43 let dir = paths.snapshots();
44 if !dir.exists() {
45 return Ok(None);
46 }
47 let mut newest: Option<PathBuf> = None;
48 for entry in fs::read_dir(&dir)? {
49 let entry = entry?;
50 let p = entry.path();
51 if p.extension().and_then(|s| s.to_str()) != Some("json") {
52 continue;
53 }
54 newest = match newest {
55 Some(cur) if cur >= p => Some(cur),
56 _ => Some(p),
57 };
58 }
59 match newest {
60 Some(p) => Ok(Some(load(&p)?)),
61 None => Ok(None),
62 }
63}
64
65fn load(path: &Path) -> Result<ScanReport> {
66 let body = fs::read_to_string(path)?;
67 let report: ScanReport =
68 serde_json::from_str(&body).map_err(|e| Error::Scan(format!("snapshot parse: {e}")))?;
69 Ok(report)
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75 use crate::scan::inventory::PathEntry;
76
77 fn sample() -> ScanReport {
78 ScanReport {
79 scanned_at: chrono::Utc::now(),
80 paths: vec![PathEntry {
81 path: "/tmp/x".into(),
82 category: "test".into(),
83 sha256: "0".repeat(64),
84 size: 1,
85 }],
86 }
87 }
88
89 fn paths_for(tmp: &tempfile::TempDir) -> Paths {
90 Paths {
91 home: tmp.path().to_path_buf(),
92 user_home: tmp.path().to_path_buf(),
93 }
94 }
95
96 #[test]
97 fn save_then_load_roundtrip() {
98 let tmp = tempfile::tempdir().unwrap();
101 let paths = paths_for(&tmp);
102 let report = sample();
103 let written = save(&paths, &report).unwrap();
104 assert!(written.exists());
105 let loaded = load_latest(&paths).unwrap().unwrap();
106 assert_eq!(loaded.paths, report.paths);
107 }
108
109 #[test]
110 fn load_latest_returns_none_when_dir_absent() {
111 let tmp = tempfile::tempdir().unwrap();
112 let paths = paths_for(&tmp);
115 assert!(load_latest(&paths).unwrap().is_none());
116 }
117}