usb_forensic/sources/
shellbag.rs1use crate::{Attribute, Claim, DeviceKey, HistorySource, Provenance, SourceKind, Value};
26use peripheral_core::shellbag::ShellbagEntry;
27
28pub struct ShellbagSource<'a> {
30 entries: &'a [ShellbagEntry],
31}
32
33impl<'a> ShellbagSource<'a> {
34 #[must_use]
36 pub fn new(entries: &'a [ShellbagEntry]) -> Self {
37 Self { entries }
38 }
39}
40
41impl HistorySource for ShellbagSource<'_> {
42 fn claims(&self) -> Vec<Claim> {
43 let mut out = Vec::new();
44 for entry in self.entries {
45 push_entry_claims(entry, &mut out);
46 }
47 out
48 }
49}
50
51fn push_entry_claims(entry: &ShellbagEntry, out: &mut Vec<Claim>) {
55 let Some(letter) = entry.drive_letter else {
56 return;
57 };
58 let drive = format!("{letter}:");
59 let device = DeviceKey(drive.clone());
60 let provenance = Provenance {
61 source: SourceKind::Shellbag,
62 locator: entry
63 .source
64 .key_path
65 .clone()
66 .unwrap_or_else(|| entry.source.file.clone()),
67 };
68
69 out.push(Claim {
73 device: device.clone(),
74 attribute: Attribute::DriveLetter,
75 value: Value::Text(drive),
76 provenance: provenance.clone(),
77 });
78 out.push(Claim {
79 device,
80 attribute: Attribute::BrowsedFolder,
81 value: Value::Text(entry.path.clone()),
82 provenance,
83 });
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89 use peripheral_core::Provenance as PcProvenance;
90
91 fn entry(path: &str, drive: Option<char>, key_path: Option<&str>) -> ShellbagEntry {
92 ShellbagEntry {
93 path: path.to_string(),
94 drive_letter: drive,
95 last_write: Some(1_600_000_000),
96 source: PcProvenance {
97 file: "NTUSER.DAT".to_string(),
98 line: 0,
99 key_path: key_path.map(ToString::to_string),
100 },
101 }
102 }
103
104 fn claims_for(entries: &[ShellbagEntry]) -> Vec<Claim> {
105 ShellbagSource::new(entries).claims()
106 }
107
108 #[test]
109 fn drive_letter_entry_yields_drive_letter_and_browsed_folder() {
110 let entries = [entry(
111 "My Computer\\E:\\\\photos",
112 Some('E'),
113 Some("Software\\Microsoft\\Windows\\Shell\\BagMRU\\0\\0\\0"),
114 )];
115 let claims = claims_for(&entries);
116 assert_eq!(claims.len(), 2);
117
118 let dl = &claims[0];
119 assert_eq!(dl.device, DeviceKey("E:".to_string()));
120 assert_eq!(dl.attribute, Attribute::DriveLetter);
121 assert_eq!(dl.value, Value::Text("E:".to_string()));
122 assert_eq!(dl.provenance.source, SourceKind::Shellbag);
123 assert_eq!(
124 dl.provenance.locator,
125 "Software\\Microsoft\\Windows\\Shell\\BagMRU\\0\\0\\0"
126 );
127
128 let bf = &claims[1];
129 assert_eq!(bf.device, DeviceKey("E:".to_string()));
130 assert_eq!(bf.attribute, Attribute::BrowsedFolder);
131 assert_eq!(
132 bf.value,
133 Value::Text("My Computer\\E:\\\\photos".to_string())
134 );
135 assert_eq!(bf.provenance.source, SourceKind::Shellbag);
136 }
137
138 #[test]
139 fn entry_without_a_drive_letter_is_skipped() {
140 let entries = [entry("My Computer\\Some Volume", None, None)];
142 assert!(claims_for(&entries).is_empty());
143 }
144
145 #[test]
146 fn locator_falls_back_to_the_file_without_a_key_path() {
147 let entries = [entry("My Computer\\E:\\", Some('E'), None)];
148 let claims = claims_for(&entries);
149 assert_eq!(claims[0].provenance.locator, "NTUSER.DAT");
150 }
151
152 #[test]
153 fn multiple_entries_accumulate() {
154 let entries = [
155 entry("My Computer\\E:\\", Some('E'), None),
156 entry("My Computer\\F:\\docs", Some('F'), None),
157 ];
158 let claims = claims_for(&entries);
159 assert_eq!(claims.len(), 4);
160 assert_eq!(claims[0].device, DeviceKey("E:".to_string()));
161 assert_eq!(claims[3].device, DeviceKey("F:".to_string()));
162 }
163
164 #[test]
165 fn browsed_folder_is_reattributed_to_the_device_mounted_at_that_drive_letter() {
166 let entries = [entry("My Computer\\E:\\\\photos", Some('E'), None)];
169 let mut all = claims_for(&entries);
170 all.push(Claim {
171 device: DeviceKey("USBSTOR-DEV-1".into()),
172 attribute: Attribute::DriveLetter,
173 value: Value::Text("E:".into()),
174 provenance: Provenance {
175 source: SourceKind::Usbstor,
176 locator: "x".into(),
177 },
178 });
179 let out = crate::reconcile_volume_serials(&all);
180 let bf = out
181 .iter()
182 .find(|c| c.attribute == Attribute::BrowsedFolder)
183 .expect("browsed folder present");
184 assert_eq!(bf.device, DeviceKey("USBSTOR-DEV-1".into()));
185 }
186}