1use crate::{Attribute, Claim, DeviceKey, HistorySource, Provenance, SourceKind, Value};
14
15const CONFIG_EVENT_IDS: [u32; 3] = [400, 410, 430];
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct KernelPnpEvent {
22 pub timestamp: String,
24 pub event_id: u32,
26 pub device_instance_id: String,
28 pub driver_name: Option<String>,
30}
31
32pub fn kernel_pnp_events<I>(records: I) -> Vec<KernelPnpEvent>
38where
39 I: IntoIterator<Item = serde_json::Value>,
40{
41 records
42 .into_iter()
43 .filter_map(|r| kernel_pnp_event(&r))
44 .collect()
45}
46
47fn kernel_pnp_event(record: &serde_json::Value) -> Option<KernelPnpEvent> {
49 let system = record.pointer("/Event/System")?;
50 if system
51 .pointer("/Provider/#attributes/Name")
52 .and_then(serde_json::Value::as_str)
53 != Some("Microsoft-Windows-Kernel-PnP")
54 {
55 return None;
56 }
57 let event_id = event_id(system)?;
58 if !CONFIG_EVENT_IDS.contains(&event_id) {
59 return None;
60 }
61 let fields = winevt_extract::flatten_event_data(record);
64 let device_instance_id = fields.get("DeviceInstanceId")?.clone();
65 if !is_usb_instance(&device_instance_id) {
66 return None;
67 }
68 let timestamp = system
69 .pointer("/TimeCreated/#attributes/SystemTime")?
70 .as_str()?
71 .to_string();
72 let driver_name = fields.get("DriverName").filter(|s| !s.is_empty()).cloned();
73 Some(KernelPnpEvent {
74 timestamp,
75 event_id,
76 device_instance_id,
77 driver_name,
78 })
79}
80
81fn event_id(system: &serde_json::Value) -> Option<u32> {
83 let raw = system.get("EventID")?;
84 let n = raw
85 .as_u64()
86 .or_else(|| raw.get("#text").and_then(serde_json::Value::as_u64))?;
87 u32::try_from(n).ok()
88}
89
90fn is_usb_instance(id: &str) -> bool {
94 (id.starts_with("USB\\") || id.starts_with("USBSTOR\\")) && !id.starts_with("USB\\ROOT_HUB")
95}
96
97pub struct KernelPnpSource<'a> {
99 events: &'a [KernelPnpEvent],
100}
101
102impl<'a> KernelPnpSource<'a> {
103 #[must_use]
105 pub fn new(events: &'a [KernelPnpEvent]) -> Self {
106 Self { events }
107 }
108}
109
110impl HistorySource for KernelPnpSource<'_> {
111 fn claims(&self) -> Vec<Claim> {
112 let mut out = Vec::new();
113 for event in self.events {
114 let start = event.device_instance_id.rfind('\\').map_or(0, |i| i + 1);
118 let serial = &event.device_instance_id[start..];
119 if serial.is_empty() {
120 continue; }
122 let Ok(when) = event.timestamp.parse::<jiff::Timestamp>() else {
124 continue;
125 };
126 out.push(Claim {
127 device: DeviceKey(serial.to_string()),
128 attribute: Attribute::LastConnected,
129 value: Value::Timestamp(when.as_second()),
130 provenance: Provenance {
131 source: SourceKind::KernelPnp,
132 locator: format!(
133 "Microsoft-Windows-Kernel-PnP/Configuration#{} {}",
134 event.event_id, event.device_instance_id
135 ),
136 },
137 });
138 }
139 out
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use serde_json::json;
147
148 fn record(provider: &str, event_id: u32, instance: &str, when: &str) -> serde_json::Value {
150 json!({
151 "Event": {
152 "System": {
153 "Provider": { "#attributes": { "Name": provider } },
154 "EventID": event_id,
155 "TimeCreated": { "#attributes": { "SystemTime": when } }
156 },
157 "EventData": {
158 "DeviceInstanceId": instance,
159 "DriverName": "usbstor.inf"
160 }
161 }
162 })
163 }
164
165 #[test]
166 fn extracts_a_usb_configuration_event() {
167 let rec = record(
168 "Microsoft-Windows-Kernel-PnP",
169 400,
170 "USB\\VID_0781&PID_5597\\4C530000261130109435",
171 "2020-09-19T04:36:42.874132Z",
172 );
173 let events = kernel_pnp_events([rec]);
174 assert_eq!(events.len(), 1);
175 assert_eq!(events[0].event_id, 400);
176 assert_eq!(
177 events[0].device_instance_id,
178 "USB\\VID_0781&PID_5597\\4C530000261130109435"
179 );
180 assert_eq!(events[0].timestamp, "2020-09-19T04:36:42.874132Z");
181 assert_eq!(events[0].driver_name.as_deref(), Some("usbstor.inf"));
182 }
183
184 #[test]
185 fn a_usbstor_disk_layer_event_is_extracted() {
186 let rec = record(
187 "Microsoft-Windows-Kernel-PnP",
188 400,
189 "USBSTOR\\Disk&Ven_SanDisk&Prod_Cruzer_Glide_3.0&Rev_1.00\\4C530000261130109435&0",
190 "2020-09-19T04:36:42.906239Z",
191 );
192 assert_eq!(kernel_pnp_events([rec]).len(), 1);
193 }
194
195 #[test]
196 fn non_kernel_pnp_provider_is_ignored() {
197 let rec = record(
198 "Microsoft-Windows-Partition",
199 400,
200 "USB\\VID_0781&PID_5597\\4C530000261130109435",
201 "2020-09-19T04:36:42Z",
202 );
203 assert!(kernel_pnp_events([rec]).is_empty());
204 }
205
206 #[test]
207 fn a_non_configuration_event_id_is_ignored() {
208 let rec = record(
209 "Microsoft-Windows-Kernel-PnP",
210 420, "USB\\VID_0781&PID_5597\\4C530000261130109435",
212 "2020-09-19T04:36:42Z",
213 );
214 assert!(kernel_pnp_events([rec]).is_empty());
215 }
216
217 #[test]
218 fn an_internal_non_usb_device_is_ignored() {
219 let rec = record(
220 "Microsoft-Windows-Kernel-PnP",
221 400,
222 "ACPI\\PNP0A03\\0",
223 "2020-09-19T04:36:42Z",
224 );
225 assert!(kernel_pnp_events([rec]).is_empty());
226 }
227
228 #[test]
229 fn a_usb_root_hub_controller_is_ignored() {
230 for id in [
232 "USB\\ROOT_HUB\\5&3bb57b&0",
233 "USB\\ROOT_HUB30\\5&d01e486&0&0",
234 ] {
235 let rec = record(
236 "Microsoft-Windows-Kernel-PnP",
237 400,
238 id,
239 "2020-09-19T04:36:42Z",
240 );
241 assert!(kernel_pnp_events([rec]).is_empty(), "{id} must be excluded");
242 }
243 }
244
245 #[test]
246 fn an_event_id_serialized_as_an_object_is_read() {
247 let mut rec = record(
249 "Microsoft-Windows-Kernel-PnP",
250 0,
251 "USB\\VID_0781&PID_5597\\4C530000261130109435",
252 "2020-09-19T04:36:42Z",
253 );
254 rec["Event"]["System"]["EventID"] =
255 json!({ "#text": 410, "#attributes": { "Qualifiers": "0" } });
256 let events = kernel_pnp_events([rec]);
257 assert_eq!(events.len(), 1);
258 assert_eq!(events[0].event_id, 410);
259 }
260
261 fn event(instance: &str, when: &str) -> KernelPnpEvent {
262 KernelPnpEvent {
263 timestamp: when.to_string(),
264 event_id: 400,
265 device_instance_id: instance.to_string(),
266 driver_name: Some("usbstor.inf".to_string()),
267 }
268 }
269
270 #[test]
271 fn source_emits_last_connected_keyed_by_the_instance_serial() {
272 let ev = event(
273 "USB\\VID_0781&PID_5597\\4C530000261130109435",
274 "2020-09-19T04:36:42.874132Z",
275 );
276 let claims = KernelPnpSource::new(std::slice::from_ref(&ev)).claims();
277 assert_eq!(claims.len(), 1);
278 assert_eq!(
280 claims[0].device,
281 DeviceKey("4C530000261130109435".to_string())
282 );
283 assert_eq!(claims[0].attribute, Attribute::LastConnected);
284 assert_eq!(claims[0].value, Value::Timestamp(1_600_490_202));
285 assert_eq!(claims[0].provenance.source, SourceKind::KernelPnp);
286 assert!(claims[0]
287 .provenance
288 .locator
289 .contains("Microsoft-Windows-Kernel-PnP/Configuration#400"));
290 }
291
292 #[test]
293 fn a_usbstor_event_keys_by_its_own_last_component() {
294 let ev = event(
295 "USBSTOR\\Disk&Ven_SanDisk&Prod_Cruzer_Glide_3.0&Rev_1.00\\4C530000261130109435&0",
296 "2020-09-19T04:36:42.906239Z",
297 );
298 let claims = KernelPnpSource::new(std::slice::from_ref(&ev)).claims();
299 assert_eq!(
301 claims[0].device,
302 DeviceKey("4C530000261130109435&0".to_string())
303 );
304 }
305
306 #[test]
307 fn a_malformed_timestamp_yields_no_claim() {
308 let ev = event("USB\\VID_0781&PID_5597\\SERIAL", "not-a-timestamp");
309 assert!(KernelPnpSource::new(std::slice::from_ref(&ev))
310 .claims()
311 .is_empty());
312 }
313
314 #[test]
315 fn an_instance_id_with_no_serial_component_yields_no_claim() {
316 let ev = event("USB\\", "2020-09-19T04:36:42Z");
318 assert!(KernelPnpSource::new(std::slice::from_ref(&ev))
319 .claims()
320 .is_empty());
321 }
322}