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
151fn civil_to_epoch(y: i64, m: i64, d: i64, hh: i64, mm: i64, ss: i64) -> Option<i64> {
154 if !(1..=12).contains(&m) || !(1..=31).contains(&d) {
155 return None;
156 }
157 if !(0..=23).contains(&hh) || !(0..=59).contains(&mm) || !(0..=60).contains(&ss) {
158 return None;
159 }
160 let y = if m <= 2 { y - 1 } else { y };
161 let era = if y >= 0 { y } else { y - 399 } / 400;
162 let yoe = y - era * 400;
163 let mp = (m + 9) % 12;
164 let doy = (153 * mp + 2) / 5 + d - 1;
165 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
166 let days = era * 146_097 + doe - 719_468;
167 Some(days * 86_400 + hh * 3_600 + mm * 60 + ss)
168}
169
170fn build_connection(
173 instance_id: &str,
174 install_epoch: Option<i64>,
175 file: &str,
176 line: usize,
177) -> Option<DeviceConnection> {
178 let mut segs = instance_id.split('\\');
179 let enumerator = segs.next()?;
180 if enumerator.is_empty() {
181 return None; }
183 let device_id = segs.next().unwrap_or("");
184 let serial_seg = segs.next();
185
186 let bus = Bus::from_enumerator(enumerator);
187 let (vid, pid) = parse_vid_pid(device_id);
188 let serial_is_os_generated = serial_seg.is_some_and(is_os_generated_serial);
189 let device_serial = serial_seg
190 .filter(|_| !serial_is_os_generated)
191 .filter(|s| !s.is_empty())
192 .map(str::to_string);
193
194 let dma_capable = bus.is_dma_capable();
195 let mut mitre = Vec::new();
196 if dma_capable {
197 mitre.push(MITRE_DMA);
198 }
199 if bus.is_mass_storage() {
200 mitre.push(MITRE_EXFIL_USB);
201 }
202
203 Some(DeviceConnection {
204 bus,
205 device_class_guid: None,
206 vid,
207 pid,
208 device_serial,
209 serial_is_os_generated,
210 friendly_name: None,
211 device_instance_id: instance_id.to_string(),
212 first_install: install_epoch.map(Stamp::authoritative),
213 last_install: install_epoch.map(Stamp::authoritative),
214 last_arrival: None,
215 last_removal: None,
216 parent_id_prefix: None,
217 volume_guid: None,
218 drive_letter: None,
219 volume_serial: None,
220 disk_signature: None,
221 dma_capable,
222 mitre,
223 source: Provenance {
224 file: file.to_string(),
225 line,
226 },
227 })
228}
229
230fn parse_vid_pid(device_id: &str) -> (Option<u16>, Option<u16>) {
233 let mut vid = None;
234 let mut pid = None;
235 for part in device_id.split('&') {
236 if let Some(hex) = part.strip_prefix("VID_") {
237 vid = u16::from_str_radix(hex_prefix(hex), 16).ok();
238 } else if let Some(hex) = part.strip_prefix("PID_") {
239 pid = u16::from_str_radix(hex_prefix(hex), 16).ok();
240 }
241 }
242 (vid, pid)
243}
244
245fn hex_prefix(s: &str) -> &str {
247 let end = s.find(|c: char| !c.is_ascii_hexdigit()).unwrap_or(s.len());
248 &s[..end]
249}
250
251fn is_os_generated_serial(serial: &str) -> bool {
256 serial.chars().nth(1) == Some('&')
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 use crate::Confidence;
263
264 const VISTA_USB: &str = "[Device Install (Hardware initiated) - USB\\VID_0781&PID_5583\\1234567890AB 2023/04/15 14:23:11.456]";
265
266 #[test]
267 fn parses_vista_usb_header() {
268 let conns = parse_setupapi(VISTA_USB, "setupapi.dev.log");
269 assert_eq!(conns.len(), 1);
270 let c = &conns[0];
271 assert_eq!(c.bus, Bus::Usb);
272 assert_eq!(c.vid, Some(0x0781));
273 assert_eq!(c.pid, Some(0x5583));
274 assert_eq!(c.device_serial.as_deref(), Some("1234567890AB"));
275 assert!(!c.serial_is_os_generated);
276 assert_eq!(c.device_instance_id, "USB\\VID_0781&PID_5583\\1234567890AB");
277 assert_eq!(c.source.file, "setupapi.dev.log");
278 assert_eq!(c.source.line, 1);
279 }
280
281 #[test]
282 fn section_marker_prefix_is_stripped() {
283 let line =
285 ">>> [Device Install (Hardware initiated) - USB\\VID_0781&PID_5583\\AB 2023/04/15 14:23:11.456]";
286 let conns = parse_setupapi(line, "f");
287 assert_eq!(conns.len(), 1, "the `>>>` section marker must be stripped");
288 assert_eq!(conns[0].vid, Some(0x0781));
289 }
290
291 #[test]
292 fn install_time_is_authoritative() {
293 let c = &parse_setupapi(VISTA_USB, "f")[0];
294 let s = c.first_install.expect("first_install present");
295 assert_eq!(s.confidence, Confidence::Authoritative);
296 assert_eq!(s.value, 1_681_568_591);
298 assert_eq!(c.last_arrival, None); assert_eq!(c.last_removal, None);
300 }
301
302 #[test]
303 fn parses_xp_header() {
304 let xp =
305 "[2005/05/12 12:34:56 1234.5678] Device Install - USB\\VID_04E8&PID_6860\\0123456789";
306 let conns = parse_setupapi(xp, "setupapi.log");
307 assert_eq!(conns.len(), 1);
308 let c = &conns[0];
309 assert_eq!(c.vid, Some(0x04E8));
310 assert_eq!(c.pid, Some(0x6860));
311 assert_eq!(c.device_serial.as_deref(), Some("0123456789"));
312 assert_eq!(c.first_install.map(|s| s.value), Some(1_115_901_296));
314 }
315
316 #[test]
317 fn os_generated_serial_is_flagged_and_not_kept_as_device_serial() {
318 let line = "[Device Install (Hardware initiated) - USBSTOR\\Disk&Ven_Generic&Prod_Flash\\7&1c2c4f0a&0 2024/01/02 03:04:05.000]";
320 let c = &parse_setupapi(line, "f")[0];
321 assert!(
322 c.serial_is_os_generated,
323 "2nd-char-& serial must be flagged"
324 );
325 assert_eq!(
326 c.device_serial, None,
327 "OS-generated serial must not be reported as a real iSerial"
328 );
329 }
330
331 #[test]
332 fn dma_bus_attaches_t1200_and_dma_flag() {
333 let line = "[Device Install (Hardware initiated) - 1394\\SONY&CAMERA\\0123 2024/01/02 03:04:05.000]";
334 let c = &parse_setupapi(line, "f")[0];
335 assert_eq!(c.bus, Bus::FireWire);
336 assert!(c.dma_capable);
337 assert!(c.mitre.contains(&MitreRef("T1200")));
338 }
339
340 #[test]
341 fn mass_storage_attaches_exfil_mitre() {
342 let c = &parse_setupapi(VISTA_USB, "f")[0];
343 assert!(c.mitre.contains(&MitreRef("T1052.001")));
344 }
345
346 #[test]
347 fn volume_serial_is_distinct_field_from_device_serial() {
348 let c = &parse_setupapi(VISTA_USB, "f")[0];
351 assert!(c.device_serial.is_some());
352 assert_eq!(c.volume_serial, None);
353 }
354
355 #[test]
356 fn parse_timestamp_rejects_malformed_components() {
357 assert_eq!(
359 parse_timestamp("2023/04/15 14:23:11.456"),
360 Some(1_681_568_591)
361 );
362 assert_eq!(parse_timestamp("2024/01/02/03 04:05:06"), None);
364 assert_eq!(parse_timestamp("2024/01/02 04:05:06:07"), None);
366 assert_eq!(parse_timestamp("2024/01/02 25:00:00"), None);
368 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]";
373 assert!(parse_setupapi(bad, "f").is_empty());
374 }
375
376 #[test]
377 fn non_matching_lines_are_skipped_never_panic() {
378 let junk = ">>> [Setup online Device Install (Hardware initiated)]\n\
379 not a header at all\n\
380 [no closing bracket\n\
381 \n\
382 [Some Note Without A Path 2024/01/02 03:04:05.000]";
383 assert!(parse_setupapi(junk, "f").is_empty());
385 }
386
387 #[test]
388 fn garbled_and_empty_input_never_panics() {
389 assert!(parse_setupapi("", "f").is_empty());
390 assert!(parse_setupapi("\u{feff}\0\\\\\\[[[]]]", "f").is_empty());
391 assert!(parse_setupapi("[USB\\VID_0781&PID_5583\\X 9999/99/99 99:99:99]", "f").is_empty());
393 }
394
395 #[test]
396 fn missing_serial_segment_yields_none_serial() {
397 let line = "[Device Install (Hardware initiated) - PCI\\VEN_8086&DEV_1234 2024/01/02 03:04:05.000]";
398 let c = &parse_setupapi(line, "f")[0];
399 assert_eq!(c.bus, Bus::Pcie);
400 assert_eq!(c.device_serial, None);
401 assert!(!c.serial_is_os_generated);
402 assert!(c.dma_capable); }
404}