Skip to main content

usb_forensic/sources/
kernel_pnp.rs

1//! Adapter: Microsoft-Windows-Kernel-PnP/Configuration event-log records → USB-history
2//! [`Claim`]s.
3//!
4//! The Kernel-PnP Configuration log records a device-configuration event each time Windows
5//! configures a Plug-and-Play device — EID 400 (started), 410 (migrated), 430 (installed).
6//! For a USB device the `DeviceInstanceId` carries the same instance serial the registry
7//! `Enum\{USB,USBSTOR}` keys record, so a Kernel-PnP event is an **event-log** witness that
8//! the device was connected at the record's time — a different tamper surface from the
9//! registry / setupapi record of the same device, so when they agree the correlation core
10//! grades it corroborated. A pure mapping over already-decoded event JSON; the `evtx` reader
11//! (in the binary) does the `.evtx` parsing.
12
13use crate::{Attribute, Claim, DeviceKey, HistorySource, Provenance, SourceKind, Value};
14
15/// Kernel-PnP device-configuration Event IDs that witness a device being present/configured:
16/// 400 (device started), 410 (device migrated), 430 (device installed).
17const CONFIG_EVENT_IDS: [u32; 3] = [400, 410, 430];
18
19/// A decoded USB Kernel-PnP configuration event.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct KernelPnpEvent {
22    /// `System/TimeCreated` in ISO-8601 UTC — the record's time.
23    pub timestamp: String,
24    /// The configuration Event ID (400 started / 410 migrated / 430 installed).
25    pub event_id: u32,
26    /// `EventData/DeviceInstanceId`, e.g. `USB\VID_0781&PID_5597\4C530000261130109435`.
27    pub device_instance_id: String,
28    /// `EventData/DriverName` when present (e.g. `usbstor.inf`) — context for the locator.
29    pub driver_name: Option<String>,
30}
31
32/// Extract USB Kernel-PnP configuration events from an iterator of `evtx` record JSON values
33/// (each the `{"Event": {…}}` object from `records_json_value()`). Keeps only
34/// `Microsoft-Windows-Kernel-PnP` records with a configuration Event ID whose
35/// `DeviceInstanceId` is a `USB\` / `USBSTOR\` device (internal ACPI/PCI/root-hub devices
36/// are dropped).
37pub 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
47/// Decode one `evtx` record; `None` unless it is a USB Kernel-PnP configuration event.
48fn 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    // `flatten_event_data` normalizes both EVTX EventData serialization shapes into a flat
62    // field map — reused from winevt-extract, the crate that owns the evtx field decoding.
63    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
81/// Read `System/EventID`, tolerating both the bare-number and `{"#text": N, …}` shapes.
82fn 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
90/// A `USB\` / `USBSTOR\` peripheral / mass-storage device instance id, excluding the host's
91/// own root hubs (`USB\ROOT_HUB*`, the USB controllers — infrastructure that is always
92/// present and carries no removable-media identity) and internal `ACPI\` / `PCI\` devices.
93fn is_usb_instance(id: &str) -> bool {
94    (id.starts_with("USB\\") || id.starts_with("USBSTOR\\")) && !id.starts_with("USB\\ROOT_HUB")
95}
96
97/// A [`HistorySource`] over decoded USB Kernel-PnP configuration events.
98pub struct KernelPnpSource<'a> {
99    events: &'a [KernelPnpEvent],
100}
101
102impl<'a> KernelPnpSource<'a> {
103    /// Wrap decoded Kernel-PnP events (from [`kernel_pnp_events`]).
104    #[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            // Key by the instance serial — the last '\'-separated component of the device
115            // instance id — identical to how the registry / setupapi sources key, so a
116            // Kernel-PnP event corroborates the registry record of the same device.
117            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; // no instance serial to key on
121            }
122            // A malformed timestamp is dropped, never turned into a bogus epoch.
123            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    /// A Kernel-PnP configuration record as `evtx` serializes it (flat `EventData`).
149    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, // a problem/error event, not a configuration witness
211            "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        // The host's own USB root hubs are infrastructure, not removable devices.
231        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        // Some providers serialize EventID as {"#text": N, "#attributes": {...}}.
248        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        // Keyed by the last '\'-component — the same key the registry USBSTOR source uses.
279        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        // The USBSTOR layer keys by `<serial>&0` — matching the registry USBSTOR Enum key.
300        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        // A trailing backslash leaves an empty last component — nothing to key on.
317        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}