usb_forensic/sources/
peripheral.rs1use crate::{Attribute, Claim, DeviceKey, HistorySource, Provenance, SourceKind, Value};
10use peripheral_core::DeviceConnection;
11
12pub struct PeripheralSource<'a> {
20 conns: &'a [DeviceConnection],
21 source: SourceKind,
22}
23
24impl<'a> PeripheralSource<'a> {
25 #[must_use]
29 pub fn new(conns: &'a [DeviceConnection], source: SourceKind) -> Self {
30 Self { conns, source }
31 }
32}
33
34impl HistorySource for PeripheralSource<'_> {
35 fn claims(&self) -> Vec<Claim> {
36 let mut out = Vec::new();
37 for conn in self.conns {
38 push_conn(conn, self.source, &mut out);
39 }
40 out
41 }
42}
43
44fn push_conn(conn: &DeviceConnection, source: SourceKind, out: &mut Vec<Claim>) {
45 let device = DeviceKey(if let Some(serial) = &conn.device_serial {
49 serial.clone()
50 } else {
51 let id = &conn.device_instance_id;
52 let start = id.rfind('\\').map_or(0, |i| i + 1);
53 id[start..].to_string()
54 });
55 let locator = conn
58 .source
59 .key_path
60 .clone()
61 .unwrap_or_else(|| format!("{}:{}", conn.source.file, conn.source.line));
62 let claim = |attribute, value: i64| Claim {
63 device: device.clone(),
64 attribute,
65 value: Value::Timestamp(value),
66 provenance: Provenance {
67 source,
68 locator: locator.clone(),
69 },
70 };
71 if let Some(s) = &conn.first_install {
72 out.push(claim(Attribute::FirstConnected, s.value));
73 }
74 if let Some(s) = &conn.last_arrival {
75 out.push(claim(Attribute::LastConnected, s.value));
76 }
77 if let Some(s) = &conn.last_removal {
78 out.push(claim(Attribute::LastRemoved, s.value));
79 }
80 if let Some(letter) = conn.drive_letter {
81 out.push(Claim {
82 device: device.clone(),
83 attribute: Attribute::DriveLetter,
84 value: Value::Text(format!("{letter}:")),
85 provenance: Provenance {
86 source,
87 locator: locator.clone(),
88 },
89 });
90 }
91 if conn.bus == peripheral_core::Bus::Mtp {
94 out.push(Claim {
95 device,
96 attribute: Attribute::DeviceClass,
97 value: Value::Text("MTP".to_string()),
98 provenance: Provenance { source, locator },
99 });
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use peripheral_core::setupapi::parse_setupapi;
107 use peripheral_core::Stamp;
108
109 const USBSTOR_HEADER: &str = "[Device Install (Hardware initiated) - \
110 USBSTOR\\Disk&Ven_Generic&Prod_Flash\\7&1c2c4f0a&0 2024/01/02 03:04:05.000]";
111
112 #[test]
113 fn setupapi_connection_yields_first_connected_claim() {
114 let conns = parse_setupapi(USBSTOR_HEADER, "setupapi.dev.log");
115 assert_eq!(conns.len(), 1);
116 let claims = PeripheralSource::new(&conns, SourceKind::SetupApi).claims();
117 let fc = claims
118 .iter()
119 .find(|c| c.attribute == Attribute::FirstConnected)
120 .expect("first-connected claim");
121 assert_eq!(fc.device, DeviceKey("7&1c2c4f0a&0".to_string()));
123 assert_eq!(fc.provenance.source, SourceKind::SetupApi);
124 assert_eq!(fc.provenance.locator, "setupapi.dev.log:1");
126 assert!(matches!(fc.value, Value::Timestamp(_)));
127 }
128
129 #[test]
130 fn source_kind_is_taken_from_the_caller() {
131 let conns = parse_setupapi(USBSTOR_HEADER, "syslog");
134 let claims = PeripheralSource::new(&conns, SourceKind::LinuxKernelLog).claims();
135 assert!(!claims.is_empty());
136 assert!(claims
137 .iter()
138 .all(|c| c.provenance.source == SourceKind::LinuxKernelLog));
139 }
140
141 #[test]
142 fn registry_key_path_is_used_as_the_locator() {
143 let mut conn = parse_setupapi(USBSTOR_HEADER, "SYSTEM")
146 .pop()
147 .expect("one conn");
148 let key = "ControlSet001\\Enum\\USBSTOR\\Disk&Ven_Generic&Prod_Flash\\7&1c2c4f0a&0";
149 conn.source.key_path = Some(key.to_string());
150 conn.source.line = 0;
151 let conns = [conn];
152 let claims = PeripheralSource::new(&conns, SourceKind::Usbstor).claims();
153 assert_eq!(claims[0].provenance.source, SourceKind::Usbstor);
154 assert_eq!(claims[0].provenance.locator, key);
155 }
156
157 #[test]
158 fn drive_letter_yields_a_drive_letter_claim() {
159 let mut conn = parse_setupapi(USBSTOR_HEADER, "SYSTEM")
163 .pop()
164 .expect("one conn");
165 conn.drive_letter = Some('E');
166 let conns = [conn];
167 let claims = PeripheralSource::new(&conns, SourceKind::Usbstor).claims();
168 let dl = claims
169 .iter()
170 .find(|c| c.attribute == Attribute::DriveLetter)
171 .expect("drive-letter claim");
172 assert_eq!(dl.value, Value::Text("E:".to_string()));
173 assert_eq!(dl.device, DeviceKey("7&1c2c4f0a&0".to_string()));
174 assert_eq!(dl.provenance.source, SourceKind::Usbstor);
175 }
176
177 #[test]
178 fn explicit_device_serial_is_used_as_the_key() {
179 let mut conn = parse_setupapi(USBSTOR_HEADER, "f").pop().expect("one conn");
180 conn.device_serial = Some("AA11BB22".to_string());
181 let conns = [conn];
182 let claims = PeripheralSource::new(&conns, SourceKind::SetupApi).claims();
183 assert_eq!(claims[0].device, DeviceKey("AA11BB22".to_string()));
184 }
185
186 #[test]
187 fn arrival_and_removal_stamps_yield_last_connected_and_removed() {
188 let mut conn = parse_setupapi(USBSTOR_HEADER, "f").pop().expect("one conn");
189 conn.last_arrival = Some(Stamp::inferred(1_700_000_500));
190 conn.last_removal = Some(Stamp::inferred(1_700_000_900));
191 let conns = [conn];
192 let claims = PeripheralSource::new(&conns, SourceKind::SetupApi).claims();
193 assert_eq!(
194 claims
195 .iter()
196 .find(|c| c.attribute == Attribute::LastConnected)
197 .map(|c| &c.value),
198 Some(&Value::Timestamp(1_700_000_500))
199 );
200 assert_eq!(
201 claims
202 .iter()
203 .find(|c| c.attribute == Attribute::LastRemoved)
204 .map(|c| &c.value),
205 Some(&Value::Timestamp(1_700_000_900))
206 );
207 }
208
209 #[test]
210 fn an_mtp_bus_device_emits_a_device_class_claim() {
211 let mut conn = parse_setupapi(USBSTOR_HEADER, "f").pop().expect("one conn");
212 conn.bus = peripheral_core::Bus::Mtp;
213 let conns = [conn];
214 let claims = PeripheralSource::new(&conns, SourceKind::Usbstor).claims();
215 let dc = claims
216 .iter()
217 .find(|c| c.attribute == Attribute::DeviceClass)
218 .expect("device-class claim");
219 assert_eq!(dc.value, Value::Text("MTP".to_string()));
220 }
221
222 #[test]
223 fn a_non_mtp_bus_device_emits_no_device_class_claim() {
224 let conns = parse_setupapi(USBSTOR_HEADER, "f"); let claims = PeripheralSource::new(&conns, SourceKind::Usbstor).claims();
226 assert!(!claims.iter().any(|c| c.attribute == Attribute::DeviceClass));
227 }
228
229 #[test]
230 fn separatorless_instance_id_is_used_whole() {
231 let mut conn = parse_setupapi(USBSTOR_HEADER, "f").pop().expect("one conn");
233 conn.device_serial = None;
234 conn.device_instance_id = "BAREINSTANCE".to_string();
235 let conns = [conn];
236 let claims = PeripheralSource::new(&conns, SourceKind::SetupApi).claims();
237 assert_eq!(claims[0].device, DeviceKey("BAREINSTANCE".to_string()));
238 }
239}