Skip to main content

usb_forensic/sources/
driver_framework.rs

1//! Adapter: Microsoft-Windows-DriverFrameworks-UserMode/Operational event-log records →
2//! USB-history [`Claim`]s.
3//!
4//! The DriverFrameworks-UserMode (UMDF) Operational log tracks the user-mode driver host's
5//! view of a device's lifecycle. For a USB device the two forensically load-bearing records
6//! are the *arrival* (EID 2003, `UMDFHostDeviceArrivalBegin`) and the *final removal*
7//! (EID 2102, `UMDFHostDeviceRequest`), correlated by the device instance serial — the same
8//! key the registry `Enum\{USB,USBSTOR}` and Kernel-PnP records use. So a DriverFrameworks
9//! record is an **event-log** witness of a connect/disconnect at the record's time, on a
10//! different tamper surface than the registry, and the correlation core grades agreement as
11//! corroborated. A pure mapping over already-decoded event JSON; the `evtx` reader (in the
12//! binary) does the `.evtx` parsing.
13//!
14//! Field structure per two independent authoritative maps — Eric Zimmerman's EvtxECmd map
15//! (`.../DriverFrameworks-UserMode_2100.map`, InstanceId under `UserData/UMDFHostDeviceRequest`)
16//! and IncideDigital rvt2 (`instance`/`lifetime` as attributes on the UserData child element).
17//! Real logs use the *attribute* form; the element form is handled defensively. EID 2003/2102
18//! chosen as the clean connect/disconnect pair (2100/2101 are intermediate power ops — noise).
19//! The log is disabled by default on Win8+, so it is present only when an admin enabled it.
20
21use crate::{Attribute, Claim, DeviceKey, HistorySource, Provenance, SourceKind, Value};
22
23/// UMDF device *arrival* — the primary connection witness (hardware ids embedded inline).
24const CONNECT_EVENT_ID: u32 = 2003;
25/// UMDF device *final removal* — the disconnect witness.
26const DISCONNECT_EVENT_ID: u32 = 2102;
27
28/// A decoded USB DriverFrameworks arrival/removal event.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct DriverFrameworkEvent {
31    /// `System/TimeCreated` in ISO-8601 UTC — the record's time.
32    pub timestamp: String,
33    /// The Event ID: 2003 (arrival/connect) or 2102 (final removal/disconnect).
34    pub event_id: u32,
35    /// The device instance id, e.g. `USB\VID_0781&PID_5597\4C530000261130109435`, from the
36    /// UserData child's `instance` attribute (or `InstanceId` element).
37    pub instance_id: String,
38}
39
40/// Extract USB DriverFrameworks arrival/removal events from an iterator of `evtx` record JSON
41/// values (each the `{"Event": {…}}` object). Keeps only
42/// `Microsoft-Windows-DriverFrameworks-UserMode` records with EID 2003 or 2102 whose instance
43/// is a `USB\` / `USBSTOR\` device (root hubs and internal devices dropped).
44pub fn driver_framework_events<I>(records: I) -> Vec<DriverFrameworkEvent>
45where
46    I: IntoIterator<Item = serde_json::Value>,
47{
48    records
49        .into_iter()
50        .filter_map(|r| driver_framework_event(&r))
51        .collect()
52}
53
54/// Decode one `evtx` record; `None` unless it is a USB DriverFrameworks arrival/removal event.
55fn driver_framework_event(record: &serde_json::Value) -> Option<DriverFrameworkEvent> {
56    let system = record.pointer("/Event/System")?;
57    if system
58        .pointer("/Provider/#attributes/Name")
59        .and_then(serde_json::Value::as_str)
60        != Some("Microsoft-Windows-DriverFrameworks-UserMode")
61    {
62        return None;
63    }
64    let event_id = event_id(system)?;
65    if event_id != CONNECT_EVENT_ID && event_id != DISCONNECT_EVENT_ID {
66        return None;
67    }
68    let instance_id = instance_id(record)?;
69    if !is_usb_instance(&instance_id) {
70        return None;
71    }
72    let timestamp = system
73        .pointer("/TimeCreated/#attributes/SystemTime")?
74        .as_str()?
75        .to_string();
76    Some(DriverFrameworkEvent {
77        timestamp,
78        event_id,
79        instance_id,
80    })
81}
82
83/// The device instance id from the UserData child element. DriverFrameworks nests it under a
84/// single child whose name varies by EID (`UMDFHostDeviceArrivalBegin` for 2003,
85/// `UMDFHostDeviceRequest` for 2100–2102); rather than hard-code the name, take the first
86/// child and read its `instance` attribute (real form) or `InstanceId` element (map form).
87fn instance_id(record: &serde_json::Value) -> Option<String> {
88    let user_data = record.pointer("/Event/UserData")?.as_object()?;
89    for child in user_data.values() {
90        if let Some(v) = child
91            .pointer("/#attributes/instance")
92            .and_then(serde_json::Value::as_str)
93        {
94            return Some(v.to_string());
95        }
96        if let Some(v) = child.get("InstanceId").and_then(serde_json::Value::as_str) {
97            return Some(v.to_string());
98        }
99    }
100    None
101}
102
103/// Read `System/EventID`, tolerating both the bare-number and `{"#text": N, …}` shapes.
104fn event_id(system: &serde_json::Value) -> Option<u32> {
105    let raw = system.get("EventID")?;
106    let n = raw
107        .as_u64()
108        .or_else(|| raw.get("#text").and_then(serde_json::Value::as_u64))?;
109    u32::try_from(n).ok()
110}
111
112/// A `USB\` / `USBSTOR\` peripheral / mass-storage device instance id, excluding the host's
113/// own root hubs and internal `ACPI\` / `PCI\` / `SWD\` devices. The `SWD\WPDBUSENUM\`
114/// symbolic-link form (used by some UMDF records) is intentionally skipped — its serial is
115/// embedded in a `#`-delimited compound with no reliable, corpus-validated split, so keying it
116/// would risk a wrong device key; the same connect is witnessed by the clean `USB\` arrival.
117fn is_usb_instance(id: &str) -> bool {
118    (id.starts_with("USB\\") || id.starts_with("USBSTOR\\")) && !id.starts_with("USB\\ROOT_HUB")
119}
120
121/// A [`HistorySource`] over decoded USB DriverFrameworks events.
122pub struct DriverFrameworkSource<'a> {
123    events: &'a [DriverFrameworkEvent],
124}
125
126impl<'a> DriverFrameworkSource<'a> {
127    /// Wrap decoded DriverFrameworks events (from [`driver_framework_events`]).
128    #[must_use]
129    pub fn new(events: &'a [DriverFrameworkEvent]) -> Self {
130        Self { events }
131    }
132}
133
134impl HistorySource for DriverFrameworkSource<'_> {
135    fn claims(&self) -> Vec<Claim> {
136        let mut out = Vec::new();
137        for event in self.events {
138            // Key by the last '\'-component of the instance id — the instance serial —
139            // identical to the registry / Kernel-PnP keying, so this corroborates them.
140            let start = event.instance_id.rfind('\\').map_or(0, |i| i + 1);
141            let serial = &event.instance_id[start..];
142            if serial.is_empty() {
143                continue;
144            }
145            let Ok(when) = event.timestamp.parse::<jiff::Timestamp>() else {
146                continue;
147            };
148            // 2003 arrival → connected; 2102 final removal → removed.
149            let attribute = if event.event_id == DISCONNECT_EVENT_ID {
150                Attribute::LastRemoved
151            } else {
152                Attribute::LastConnected
153            };
154            out.push(Claim {
155                device: DeviceKey(serial.to_string()),
156                attribute,
157                value: Value::Timestamp(when.as_second()),
158                provenance: Provenance {
159                    source: SourceKind::DriverFramework,
160                    locator: format!(
161                        "Microsoft-Windows-DriverFrameworks-UserMode/Operational#{} {}",
162                        event.event_id, event.instance_id
163                    ),
164                },
165            });
166        }
167        out
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174    use serde_json::json;
175
176    /// A DriverFrameworks record as `evtx` serializes it: UserData with a single child element
177    /// (`root`) carrying `instance`/`lifetime` attributes.
178    fn record(
179        provider: &str,
180        event_id: u32,
181        root: &str,
182        instance: &str,
183        when: &str,
184    ) -> serde_json::Value {
185        json!({
186            "Event": {
187                "System": {
188                    "Provider": { "#attributes": { "Name": provider } },
189                    "EventID": event_id,
190                    "TimeCreated": { "#attributes": { "SystemTime": when } }
191                },
192                "UserData": {
193                    root: {
194                        "#attributes": {
195                            "instance": instance,
196                            "lifetime": "{6c1e8fd0-0000-0000-0000-000000000000}"
197                        }
198                    }
199                }
200            }
201        })
202    }
203
204    #[test]
205    fn extracts_a_2003_arrival_event() {
206        let rec = record(
207            "Microsoft-Windows-DriverFrameworks-UserMode",
208            2003,
209            "UMDFHostDeviceArrivalBegin",
210            "USB\\VID_0781&PID_5597\\4C530000261130109435",
211            "2020-09-19T04:36:42.874132Z",
212        );
213        let events = driver_framework_events([rec]);
214        assert_eq!(events.len(), 1);
215        assert_eq!(events[0].event_id, 2003);
216        assert_eq!(
217            events[0].instance_id,
218            "USB\\VID_0781&PID_5597\\4C530000261130109435"
219        );
220        assert_eq!(events[0].timestamp, "2020-09-19T04:36:42.874132Z");
221    }
222
223    #[test]
224    fn extracts_a_2102_removal_event() {
225        let rec = record(
226            "Microsoft-Windows-DriverFrameworks-UserMode",
227            2102,
228            "UMDFHostDeviceRequest",
229            "USB\\VID_0781&PID_5597\\4C530000261130109435",
230            "2020-09-19T05:10:00Z",
231        );
232        let events = driver_framework_events([rec]);
233        assert_eq!(events.len(), 1);
234        assert_eq!(events[0].event_id, 2102);
235    }
236
237    #[test]
238    fn reads_the_instanceid_element_form() {
239        // EvtxECmd's map documents an `<InstanceId>` child-element variant; handle it too.
240        let rec = json!({
241            "Event": {
242                "System": {
243                    "Provider": { "#attributes": { "Name": "Microsoft-Windows-DriverFrameworks-UserMode" } },
244                    "EventID": 2003,
245                    "TimeCreated": { "#attributes": { "SystemTime": "2020-09-19T04:36:42Z" } }
246                },
247                "UserData": {
248                    "UMDFHostDeviceArrivalBegin": {
249                        "InstanceId": "USBSTOR\\Disk&Ven_SanDisk&Prod_Cruzer&Rev_1.00\\4C530000261130109435&0",
250                        "LifetimeId": "{guid}"
251                    }
252                }
253            }
254        });
255        let events = driver_framework_events([rec]);
256        assert_eq!(events.len(), 1);
257        assert_eq!(
258            events[0].instance_id,
259            "USBSTOR\\Disk&Ven_SanDisk&Prod_Cruzer&Rev_1.00\\4C530000261130109435&0"
260        );
261    }
262
263    #[test]
264    fn a_userdata_child_with_no_instance_field_yields_no_event() {
265        // UserData present but the child carries neither an `instance` attribute nor an
266        // `InstanceId` element — nothing to key on, so the record is dropped.
267        let rec = json!({
268            "Event": {
269                "System": {
270                    "Provider": { "#attributes": { "Name": "Microsoft-Windows-DriverFrameworks-UserMode" } },
271                    "EventID": 2003,
272                    "TimeCreated": { "#attributes": { "SystemTime": "2020-09-19T04:36:42Z" } }
273                },
274                "UserData": {
275                    "UMDFHostDeviceArrivalBegin": { "LifetimeId": "{guid}" }
276                }
277            }
278        });
279        assert!(driver_framework_events([rec]).is_empty());
280    }
281
282    #[test]
283    fn non_driver_framework_provider_is_ignored() {
284        let rec = record(
285            "Microsoft-Windows-Kernel-PnP",
286            2003,
287            "UMDFHostDeviceArrivalBegin",
288            "USB\\VID_0781&PID_5597\\4C530000261130109435",
289            "2020-09-19T04:36:42Z",
290        );
291        assert!(driver_framework_events([rec]).is_empty());
292    }
293
294    #[test]
295    fn an_intermediate_power_event_id_is_ignored() {
296        // 2100/2101 are intermediate PnP/power operations — noise, not a connect/disconnect.
297        let rec = record(
298            "Microsoft-Windows-DriverFrameworks-UserMode",
299            2100,
300            "UMDFHostDeviceRequest",
301            "USB\\VID_0781&PID_5597\\4C530000261130109435",
302            "2020-09-19T04:36:42Z",
303        );
304        assert!(driver_framework_events([rec]).is_empty());
305    }
306
307    #[test]
308    fn an_internal_non_usb_device_is_ignored() {
309        let rec = record(
310            "Microsoft-Windows-DriverFrameworks-UserMode",
311            2003,
312            "UMDFHostDeviceArrivalBegin",
313            "ACPI\\PNP0A03\\0",
314            "2020-09-19T04:36:42Z",
315        );
316        assert!(driver_framework_events([rec]).is_empty());
317    }
318
319    #[test]
320    fn a_wpdbusenum_symbolic_link_instance_is_skipped() {
321        // The SWD\WPDBUSENUM\ symbolic-link form embeds the serial in a #-compound with no
322        // corpus-validated split; skip it rather than risk a wrong device key.
323        let rec = record(
324            "Microsoft-Windows-DriverFrameworks-UserMode",
325            2100,
326            "UMDFHostDeviceRequest",
327            "SWD\\WPDBUSENUM\\_??_USBSTOR#DISK&VEN_SANDISK#4C53...&0#{guid}",
328            "2020-09-19T04:36:42Z",
329        );
330        assert!(driver_framework_events([rec]).is_empty());
331    }
332
333    #[test]
334    fn a_usb_root_hub_controller_is_ignored() {
335        let rec = record(
336            "Microsoft-Windows-DriverFrameworks-UserMode",
337            2003,
338            "UMDFHostDeviceArrivalBegin",
339            "USB\\ROOT_HUB30\\5&d01e486&0&0",
340            "2020-09-19T04:36:42Z",
341        );
342        assert!(driver_framework_events([rec]).is_empty());
343    }
344
345    #[test]
346    fn an_event_id_serialized_as_an_object_is_read() {
347        let mut rec = record(
348            "Microsoft-Windows-DriverFrameworks-UserMode",
349            0,
350            "UMDFHostDeviceArrivalBegin",
351            "USB\\VID_0781&PID_5597\\4C530000261130109435",
352            "2020-09-19T04:36:42Z",
353        );
354        rec["Event"]["System"]["EventID"] =
355            json!({ "#text": 2003, "#attributes": { "Qualifiers": "0" } });
356        let events = driver_framework_events([rec]);
357        assert_eq!(events.len(), 1);
358        assert_eq!(events[0].event_id, 2003);
359    }
360
361    fn event(event_id: u32, instance: &str, when: &str) -> DriverFrameworkEvent {
362        DriverFrameworkEvent {
363            timestamp: when.to_string(),
364            event_id,
365            instance_id: instance.to_string(),
366        }
367    }
368
369    #[test]
370    fn arrival_emits_last_connected_keyed_by_serial() {
371        let ev = event(
372            2003,
373            "USB\\VID_0781&PID_5597\\4C530000261130109435",
374            "2020-09-19T04:36:42.874132Z",
375        );
376        let claims = DriverFrameworkSource::new(std::slice::from_ref(&ev)).claims();
377        assert_eq!(claims.len(), 1);
378        assert_eq!(
379            claims[0].device,
380            DeviceKey("4C530000261130109435".to_string())
381        );
382        assert_eq!(claims[0].attribute, Attribute::LastConnected);
383        assert_eq!(claims[0].value, Value::Timestamp(1_600_490_202));
384        assert_eq!(claims[0].provenance.source, SourceKind::DriverFramework);
385        assert!(claims[0]
386            .provenance
387            .locator
388            .contains("DriverFrameworks-UserMode/Operational#2003"));
389    }
390
391    #[test]
392    fn removal_emits_last_removed() {
393        let ev = event(
394            2102,
395            "USB\\VID_0781&PID_5597\\4C530000261130109435",
396            "2020-09-19T05:10:00Z",
397        );
398        let claims = DriverFrameworkSource::new(std::slice::from_ref(&ev)).claims();
399        assert_eq!(claims.len(), 1);
400        assert_eq!(claims[0].attribute, Attribute::LastRemoved);
401    }
402
403    #[test]
404    fn a_usbstor_instance_keys_by_its_own_last_component() {
405        let ev = event(
406            2003,
407            "USBSTOR\\Disk&Ven_SanDisk&Prod_Cruzer&Rev_1.00\\4C530000261130109435&0",
408            "2020-09-19T04:36:42Z",
409        );
410        let claims = DriverFrameworkSource::new(std::slice::from_ref(&ev)).claims();
411        assert_eq!(
412            claims[0].device,
413            DeviceKey("4C530000261130109435&0".to_string())
414        );
415    }
416
417    #[test]
418    fn a_malformed_timestamp_yields_no_claim() {
419        let ev = event(2003, "USB\\VID_0781&PID_5597\\SERIAL", "not-a-timestamp");
420        assert!(DriverFrameworkSource::new(std::slice::from_ref(&ev))
421            .claims()
422            .is_empty());
423    }
424
425    #[test]
426    fn an_instance_id_with_no_serial_component_yields_no_claim() {
427        let ev = event(2003, "USB\\", "2020-09-19T04:36:42Z");
428        assert!(DriverFrameworkSource::new(std::slice::from_ref(&ev))
429            .claims()
430            .is_empty());
431    }
432
433    #[test]
434    fn source_kind_lives_in_the_event_log_container() {
435        assert_eq!(
436            SourceKind::DriverFramework.container(),
437            crate::ArtifactContainer::EventLog
438        );
439        assert!(!SourceKind::DriverFramework.clock_is_local());
440    }
441}