Skip to main content

winreg_artifacts/
shellbags.rs

1//! ShellBags registry artifact extractor.
2//!
3//! ShellBags record folder navigation history in Windows. BagMRU keys hold
4//! slot values (numeric names "0", "1", ...) containing binary ShellItem data,
5//! and a `MRUListEx` value encoding the access order.
6//!
7//! This implementation walks BagMRU keys recursively and emits one
8//! `ShellbagEntry` per key. Full ShellItem binary parsing is out of scope;
9//! slot data is represented as a human-readable size preview.
10
11use std::io::Cursor;
12
13use winreg_core::hive::Hive;
14use winreg_core::key::{filetime_to_datetime, Key};
15
16// ---------------------------------------------------------------------------
17// Output type
18// ---------------------------------------------------------------------------
19
20/// A single BagMRU entry from the ShellBags registry area.
21#[derive(Debug, Clone, serde::Serialize)]
22pub struct ShellbagEntry {
23    /// Reconstructed / descriptive folder path.
24    /// For this implementation, slot data is represented as
25    /// `"BagMRU[slot=N, size=M bytes]"` for each numeric slot value present,
26    /// or an empty string if no slot values exist.
27    pub path: String,
28    /// Registry path to this BagMRU key (relative to hive root).
29    pub key_path: String,
30    /// Key `LastWriteTime` as ISO 8601, or `None` if unavailable.
31    pub last_written: Option<String>,
32    /// Decoded MRU order from `MRUListEx` (slot index strings),
33    /// terminator (0xFFFFFFFF) is excluded. Empty if value is absent.
34    pub mru_order: Vec<String>,
35}
36
37// ---------------------------------------------------------------------------
38// BagMRU candidate paths to probe (NTUSER.DAT and USRCLASS.DAT variants)
39// ---------------------------------------------------------------------------
40
41const BAGMRU_PATHS: &[&str] = &[
42    "Software\\Microsoft\\Windows\\Shell\\BagMRU",
43    "Software\\Microsoft\\Windows\\ShellNoRoam\\BagMRU",
44    "Local Settings\\Software\\Microsoft\\Windows\\Shell\\BagMRU",
45];
46
47// ---------------------------------------------------------------------------
48// Public parse function
49// ---------------------------------------------------------------------------
50
51/// Extract all ShellBag entries from a hive.
52///
53/// Probes several well-known BagMRU key paths. For each that exists, walks
54/// the key tree recursively and emits one [`ShellbagEntry`] per key.
55///
56/// Returns an empty `Vec` if no BagMRU key is present.
57pub fn parse(hive: &Hive<Cursor<Vec<u8>>>) -> Vec<ShellbagEntry> {
58    let mut entries = Vec::new();
59
60    for &path in BAGMRU_PATHS {
61        if let Ok(Some(root)) = hive.open_key(path) {
62            walk_key(&root, path, &mut entries);
63        }
64    }
65
66    entries
67}
68
69// ---------------------------------------------------------------------------
70// Recursive key walker
71// ---------------------------------------------------------------------------
72
73fn walk_key(key: &Key<'_>, key_path: &str, entries: &mut Vec<ShellbagEntry>) {
74    let last_written = filetime_to_datetime(key.last_written_raw())
75        .map(|dt| dt.format("%Y-%m-%dT%H:%M:%SZ").to_string());
76
77    // Decode MRUListEx value
78    let mru_order = decode_mrulistex(key);
79
80    // Build a path description from numeric slot values.
81    let path = build_slot_path(key);
82
83    entries.push(ShellbagEntry {
84        path,
85        key_path: key_path.to_string(),
86        last_written,
87        mru_order,
88    });
89
90    // Recurse into subkeys
91    if let Ok(subkeys) = key.subkeys() {
92        for subkey in subkeys {
93            let child_path = format!("{}\\{}", key_path, subkey.name());
94            walk_key(&subkey, &child_path, entries);
95        }
96    }
97}
98
99// ---------------------------------------------------------------------------
100// MRUListEx decoder
101// ---------------------------------------------------------------------------
102
103/// Decode `MRUListEx`: a `REG_BINARY` value holding an array of `u32` LE
104/// slot indices, terminated by `0xFFFF_FFFF`.
105fn decode_mrulistex(key: &Key<'_>) -> Vec<String> {
106    let val = match key.value("MRUListEx") {
107        Ok(Some(v)) => v,
108        _ => return Vec::new(),
109    };
110    let raw = match val.raw_data() {
111        Ok(d) => d,
112        Err(_) => return Vec::new(),
113    };
114
115    let mut order = Vec::new();
116    let mut i = 0;
117    while i + 4 <= raw.len() {
118        let slot = u32::from_le_bytes([raw[i], raw[i + 1], raw[i + 2], raw[i + 3]]);
119        if slot == 0xFFFF_FFFF {
120            break;
121        }
122        order.push(slot.to_string());
123        i += 4;
124    }
125    order
126}
127
128// ---------------------------------------------------------------------------
129// Slot path builder
130// ---------------------------------------------------------------------------
131
132/// Build a descriptive path string from numeric slot values in this key.
133///
134/// Numeric value names ("0", "1", ...) contain binary ShellItem blobs.
135/// Full parsing is out of scope; each slot is represented as
136/// `"BagMRU[slot=N, size=M bytes]"`.
137fn build_slot_path(key: &Key<'_>) -> String {
138    let values = match key.values() {
139        Ok(v) => v,
140        Err(_) => return String::new(),
141    };
142
143    let mut parts: Vec<String> = Vec::new();
144    for val in values {
145        let name = val.name();
146        // Numeric names are slot entries (skip "MRUListEx" and others).
147        if name.chars().all(|c| c.is_ascii_digit()) {
148            let size = val.data_size() as usize;
149            parts.push(format!("BagMRU[slot={name}, size={size} bytes]"));
150        }
151    }
152
153    parts.join("; ")
154}