1use crate::{Bus, DeviceConnection, MitreRef, Provenance, Stamp};
20
21const MITRE_DMA: MitreRef = MitreRef("T1200");
24const MITRE_EXFIL_USB: MitreRef = MitreRef("T1052.001");
25
26#[must_use]
32pub fn parse_setupapi(text: &str, file: &str) -> Vec<DeviceConnection> {
33 let mut out = Vec::new();
34 for (idx, line) in text.lines().enumerate() {
35 let trimmed = line.trim().trim_start_matches(['>', '<', ' ', '\t']);
38 if !trimmed.starts_with('[') {
39 continue;
40 }
41 let Some((instance_id, install_ts)) = parse_header(trimmed) else {
42 continue;
43 };
44 let Some(conn) = build_connection(&instance_id, install_ts, file, idx + 1) else {
45 continue; };
47 out.push(conn);
48 }
49 out
50}
51
52fn parse_header(line: &str) -> Option<(String, Option<i64>)> {
56 let inner = line.strip_prefix('[')?;
57 let close = inner.find(']')?;
58 let body = &inner[..close];
59
60 if let Some((desc, ts)) = split_trailing_timestamp(body) {
62 let instance = extract_instance_id(desc);
63 if let Some(instance) = instance {
65 return Some((instance, ts));
66 }
67 }
68
69 if let Some((ts, _rest)) = split_leading_timestamp(body) {
71 let after = &inner[close + 1..];
73 let instance = extract_instance_id(after)?;
74 return Some((instance, ts));
75 }
76
77 None
78}
79
80fn split_trailing_timestamp(body: &str) -> Option<(&str, Option<i64>)> {
84 let body = body.trim_end();
87 let mut it = body.rsplitn(3, char::is_whitespace);
88 let time = it.next()?;
89 let date = it.next()?;
90 let head = it.next()?;
91 let ts_str = format!("{date} {time}");
92 let epoch = parse_timestamp(&ts_str)?;
93 Some((head, Some(epoch)))
94}
95
96fn split_leading_timestamp(body: &str) -> Option<(Option<i64>, &str)> {
100 let mut it = body.splitn(3, char::is_whitespace);
101 let date = it.next()?;
102 let time = it.next()?;
103 let rest = it.next().unwrap_or("");
104 let ts_str = format!("{date} {time}");
105 let epoch = parse_timestamp(&ts_str)?;
106 Some((Some(epoch), rest))
107}
108
109fn extract_instance_id(text: &str) -> Option<String> {
113 text.split_whitespace()
116 .filter(|tok| tok.contains('\\'))
117 .filter(|tok| {
118 tok.split('\\')
119 .next()
120 .is_some_and(|e| !e.is_empty() && e.chars().all(|c| c.is_ascii_alphanumeric()))
121 })
122 .max_by_key(|tok| tok.len())
123 .map(str::to_string)
124}
125
126fn parse_timestamp(s: &str) -> Option<i64> {
130 let s = s.trim();
131 let (date, rest) = s.split_once(' ')?;
132 let mut dparts = date.split('/');
133 let year: i64 = dparts.next()?.parse().ok()?;
134 let month: i64 = dparts.next()?.parse().ok()?;
135 let day: i64 = dparts.next()?.parse().ok()?;
136 if dparts.next().is_some() {
137 return None;
138 }
139 let time = rest.split('.').next()?;
141 let mut tparts = time.split(':');
142 let hour: i64 = tparts.next()?.parse().ok()?;
143 let min: i64 = tparts.next()?.parse().ok()?;
144 let sec: i64 = tparts.next()?.parse().ok()?;
145 if tparts.next().is_some() {
146 return None;
147 }
148 civil_to_epoch(year, month, day, hour, min, sec)
149}
150
151pub(crate) fn civil_to_epoch(y: i64, m: i64, d: i64, hh: i64, mm: i64, ss: i64) -> Option<i64> {
154 if !(1..=9999).contains(&y) {
157 return None;
158 }
159 if !(1..=12).contains(&m) || !(1..=31).contains(&d) {
160 return None;
161 }
162 if !(0..=23).contains(&hh) || !(0..=59).contains(&mm) || !(0..=60).contains(&ss) {
163 return None;
164 }
165 let y = if m <= 2 { y - 1 } else { y };
166 let era = if y >= 0 { y } else { y - 399 } / 400;
167 let yoe = y - era * 400;
168 let mp = (m + 9) % 12;
169 let doy = (153 * mp + 2) / 5 + d - 1;
170 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
171 let days = era * 146_097 + doe - 719_468;
172 Some(days * 86_400 + hh * 3_600 + mm * 60 + ss)
173}
174
175fn build_connection(
178 instance_id: &str,
179 install_epoch: Option<i64>,
180 file: &str,
181 line: usize,
182) -> Option<DeviceConnection> {
183 let mut segs = instance_id.split('\\');
184 let enumerator = segs.next()?;
185 if enumerator.is_empty() {
186 return None; }
188 let device_id = segs.next().unwrap_or("");
189 let serial_seg = segs.next();
190
191 let bus = Bus::from_enumerator(enumerator);
192 let (vid, pid) = parse_vid_pid(device_id);
193 let serial_is_os_generated = serial_seg.is_some_and(is_os_generated_serial);
194 let device_serial = serial_seg
195 .filter(|_| !serial_is_os_generated)
196 .filter(|s| !s.is_empty())
197 .map(str::to_string);
198
199 let dma_capable = bus.is_dma_capable();
200 let mut mitre = Vec::new();
201 if dma_capable {
202 mitre.push(MITRE_DMA);
203 }
204 if bus.is_mass_storage() {
205 mitre.push(MITRE_EXFIL_USB);
206 }
207
208 Some(DeviceConnection {
209 bus,
210 device_class_guid: None,
211 vid,
212 pid,
213 device_serial,
214 serial_is_os_generated,
215 friendly_name: None,
216 device_instance_id: instance_id.to_string(),
217 first_install: install_epoch.map(Stamp::authoritative),
218 last_install: install_epoch.map(Stamp::authoritative),
219 last_arrival: None,
220 last_removal: None,
221 parent_id_prefix: None,
222 volume_guid: None,
223 drive_letter: None,
224 volume_serial: None,
225 disk_signature: None,
226 dma_capable,
227 mitre,
228 source: Provenance {
229 file: file.to_string(),
230 line,
231 key_path: None,
232 },
233 })
234}
235
236fn parse_vid_pid(device_id: &str) -> (Option<u16>, Option<u16>) {
239 let mut vid = None;
240 let mut pid = None;
241 for part in device_id.split('&') {
242 if let Some(hex) = part.strip_prefix("VID_") {
243 vid = u16::from_str_radix(hex_prefix(hex), 16).ok();
244 } else if let Some(hex) = part.strip_prefix("PID_") {
245 pid = u16::from_str_radix(hex_prefix(hex), 16).ok();
246 }
247 }
248 (vid, pid)
249}
250
251fn hex_prefix(s: &str) -> &str {
253 let end = s.find(|c: char| !c.is_ascii_hexdigit()).unwrap_or(s.len());
254 &s[..end]
255}
256
257fn is_os_generated_serial(serial: &str) -> bool {
262 serial.chars().nth(1) == Some('&')
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268 use crate::Confidence;
269
270 const VISTA_USB: &str = "[Device Install (Hardware initiated) - USB\\VID_0781&PID_5583\\1234567890AB 2023/04/15 14:23:11.456]";
271
272 #[test]
273 fn parses_vista_usb_header() {
274 let conns = parse_setupapi(VISTA_USB, "setupapi.dev.log");
275 assert_eq!(conns.len(), 1);
276 let c = &conns[0];
277 assert_eq!(c.bus, Bus::Usb);
278 assert_eq!(c.vid, Some(0x0781));
279 assert_eq!(c.pid, Some(0x5583));
280 assert_eq!(c.device_serial.as_deref(), Some("1234567890AB"));
281 assert!(!c.serial_is_os_generated);
282 assert_eq!(c.device_instance_id, "USB\\VID_0781&PID_5583\\1234567890AB");
283 assert_eq!(c.source.file, "setupapi.dev.log");
284 assert_eq!(c.source.line, 1);
285 }
286
287 #[test]
288 fn section_marker_prefix_is_stripped() {
289 let line =
291 ">>> [Device Install (Hardware initiated) - USB\\VID_0781&PID_5583\\AB 2023/04/15 14:23:11.456]";
292 let conns = parse_setupapi(line, "f");
293 assert_eq!(conns.len(), 1, "the `>>>` section marker must be stripped");
294 assert_eq!(conns[0].vid, Some(0x0781));
295 }
296
297 #[test]
298 fn install_time_is_authoritative() {
299 let c = &parse_setupapi(VISTA_USB, "f")[0];
300 let s = c.first_install.expect("first_install present");
301 assert_eq!(s.confidence, Confidence::Authoritative);
302 assert_eq!(s.value, 1_681_568_591);
304 assert_eq!(c.last_arrival, None); assert_eq!(c.last_removal, None);
306 }
307
308 #[test]
309 fn parses_xp_header() {
310 let xp =
311 "[2005/05/12 12:34:56 1234.5678] Device Install - USB\\VID_04E8&PID_6860\\0123456789";
312 let conns = parse_setupapi(xp, "setupapi.log");
313 assert_eq!(conns.len(), 1);
314 let c = &conns[0];
315 assert_eq!(c.vid, Some(0x04E8));
316 assert_eq!(c.pid, Some(0x6860));
317 assert_eq!(c.device_serial.as_deref(), Some("0123456789"));
318 assert_eq!(c.first_install.map(|s| s.value), Some(1_115_901_296));
320 }
321
322 #[test]
323 fn os_generated_serial_is_flagged_and_not_kept_as_device_serial() {
324 let line = "[Device Install (Hardware initiated) - USBSTOR\\Disk&Ven_Generic&Prod_Flash\\7&1c2c4f0a&0 2024/01/02 03:04:05.000]";
326 let c = &parse_setupapi(line, "f")[0];
327 assert!(
328 c.serial_is_os_generated,
329 "2nd-char-& serial must be flagged"
330 );
331 assert_eq!(
332 c.device_serial, None,
333 "OS-generated serial must not be reported as a real iSerial"
334 );
335 }
336
337 #[test]
338 fn dma_bus_attaches_t1200_and_dma_flag() {
339 let line = "[Device Install (Hardware initiated) - 1394\\SONY&CAMERA\\0123 2024/01/02 03:04:05.000]";
340 let c = &parse_setupapi(line, "f")[0];
341 assert_eq!(c.bus, Bus::FireWire);
342 assert!(c.dma_capable);
343 assert!(c.mitre.contains(&MitreRef("T1200")));
344 }
345
346 #[test]
347 fn mass_storage_attaches_exfil_mitre() {
348 let c = &parse_setupapi(VISTA_USB, "f")[0];
349 assert!(c.mitre.contains(&MitreRef("T1052.001")));
350 }
351
352 #[test]
353 fn volume_serial_is_distinct_field_from_device_serial() {
354 let c = &parse_setupapi(VISTA_USB, "f")[0];
357 assert!(c.device_serial.is_some());
358 assert_eq!(c.volume_serial, None);
359 }
360
361 #[test]
362 fn civil_to_epoch_rejects_out_of_range_year_without_overflow() {
363 assert_eq!(civil_to_epoch(9_999_999_999, 1, 1, 0, 0, 0), None);
365 assert_eq!(civil_to_epoch(0, 1, 1, 0, 0, 0), None); assert_eq!(civil_to_epoch(2023, 4, 15, 14, 23, 11), Some(1_681_568_591));
367 }
369
370 #[test]
371 fn parse_timestamp_rejects_malformed_components() {
372 assert_eq!(
374 parse_timestamp("2023/04/15 14:23:11.456"),
375 Some(1_681_568_591)
376 );
377 assert_eq!(parse_timestamp("2024/01/02/03 04:05:06"), None);
379 assert_eq!(parse_timestamp("2024/01/02 04:05:06:07"), None);
381 assert_eq!(parse_timestamp("2024/01/02 25:00:00"), None);
383 assert_eq!(parse_timestamp("2024/01/02 00:60:00"), None); assert_eq!(parse_timestamp("2024/01/02 00:00:61"), None); let bad = "[Device Install - USB\\VID_0781&PID_5583\\X 2024/01/02 25:00:00]";
388 assert!(parse_setupapi(bad, "f").is_empty());
389 }
390
391 #[test]
392 fn non_matching_lines_are_skipped_never_panic() {
393 let junk = ">>> [Setup online Device Install (Hardware initiated)]\n\
394 not a header at all\n\
395 [no closing bracket\n\
396 \n\
397 [Some Note Without A Path 2024/01/02 03:04:05.000]";
398 assert!(parse_setupapi(junk, "f").is_empty());
400 }
401
402 #[test]
403 fn garbled_and_empty_input_never_panics() {
404 assert!(parse_setupapi("", "f").is_empty());
405 assert!(parse_setupapi("\u{feff}\0\\\\\\[[[]]]", "f").is_empty());
406 assert!(parse_setupapi("[USB\\VID_0781&PID_5583\\X 9999/99/99 99:99:99]", "f").is_empty());
408 }
409
410 #[test]
411 fn missing_serial_segment_yields_none_serial() {
412 let line = "[Device Install (Hardware initiated) - PCI\\VEN_8086&DEV_1234 2024/01/02 03:04:05.000]";
413 let c = &parse_setupapi(line, "f")[0];
414 assert_eq!(c.bus, Bus::Pcie);
415 assert_eq!(c.device_serial, None);
416 assert!(!c.serial_is_os_generated);
417 assert!(c.dma_capable); }
419}