agentsec_core/scan/
diff.rs1use crate::scan::ScanReport;
11use crate::scan::inventory::PathEntry;
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::path::PathBuf;
15
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
18pub struct DiffReport {
19 pub added: Vec<PathEntry>,
21 pub modified: Vec<Change>,
23 pub removed: Vec<PathEntry>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
29pub struct Change {
30 pub path: PathBuf,
32 pub prev_sha256: String,
34 pub curr_sha256: String,
36 pub prev_size: u64,
38 pub curr_size: u64,
40}
41
42pub fn compute(prev: &ScanReport, curr: &ScanReport) -> DiffReport {
48 let prev_map: HashMap<&PathBuf, &PathEntry> = prev.paths.iter().map(|e| (&e.path, e)).collect();
49 let curr_map: HashMap<&PathBuf, &PathEntry> = curr.paths.iter().map(|e| (&e.path, e)).collect();
50
51 let mut added = Vec::new();
52 let mut modified = Vec::new();
53 let mut removed = Vec::new();
54
55 for (path, curr_entry) in &curr_map {
56 match prev_map.get(path) {
57 None => added.push((*curr_entry).clone()),
58 Some(prev_entry) if prev_entry.sha256 != curr_entry.sha256 => {
59 modified.push(Change {
60 path: (*path).clone(),
61 prev_sha256: prev_entry.sha256.clone(),
62 curr_sha256: curr_entry.sha256.clone(),
63 prev_size: prev_entry.size,
64 curr_size: curr_entry.size,
65 });
66 }
67 _ => {}
68 }
69 }
70
71 for (path, prev_entry) in &prev_map {
72 if !curr_map.contains_key(path) {
73 removed.push((*prev_entry).clone());
74 }
75 }
76
77 added.sort_by(|a, b| a.path.cmp(&b.path));
78 modified.sort_by(|a, b| a.path.cmp(&b.path));
79 removed.sort_by(|a, b| a.path.cmp(&b.path));
80
81 DiffReport {
82 added,
83 modified,
84 removed,
85 }
86}
87
88impl DiffReport {
89 pub fn is_empty(&self) -> bool {
91 self.added.is_empty() && self.modified.is_empty() && self.removed.is_empty()
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 fn entry(p: &str, sha: &str, size: u64) -> PathEntry {
100 PathEntry {
101 path: p.into(),
102 category: "x".into(),
103 sha256: sha.repeat(8),
104 size,
105 }
106 }
107
108 fn report(entries: Vec<PathEntry>) -> ScanReport {
109 ScanReport {
110 scanned_at: chrono::Utc::now(),
111 paths: entries,
112 }
113 }
114
115 #[test]
116 fn empty_diff() {
117 let r = report(vec![entry("/a", "11111111", 1)]);
118 let d = compute(&r, &r);
119 assert!(d.is_empty());
120 }
121
122 #[test]
123 fn detects_added_modified_removed() {
124 let prev = report(vec![entry("/a", "11111111", 1), entry("/b", "22222222", 2)]);
125 let curr = report(vec![
126 entry("/a", "33333333", 1), entry("/c", "44444444", 3), ]);
130 let d = compute(&prev, &curr);
131 assert_eq!(d.added.len(), 1);
132 assert_eq!(d.added[0].path, PathBuf::from("/c"));
133 assert_eq!(d.modified.len(), 1);
134 assert_eq!(d.modified[0].path, PathBuf::from("/a"));
135 assert_eq!(d.removed.len(), 1);
136 assert_eq!(d.removed[0].path, PathBuf::from("/b"));
137 }
138}