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", ...) each hold a binary ShellItem blob. Each
135/// slot is decoded with the [`shellitem`] primitive to its real folder name
136/// (volume, folder, file entry). When a slot does not decode to a named item
137/// (truncated or unrecognised class), it degrades to the `BagMRU[slot=N,
138/// size=M bytes]` preview so the slot is never silently dropped.
139fn build_slot_path(key: &Key<'_>) -> String {
140    let values = match key.values() {
141        Ok(v) => v,
142        Err(_) => return String::new(),
143    };
144
145    let mut parts: Vec<String> = Vec::new();
146    for val in values {
147        let name = val.name();
148        // Numeric names are slot entries (skip "MRUListEx" and others).
149        if name.chars().all(|c| c.is_ascii_digit()) {
150            parts.push(decode_slot(&name, &val));
151        }
152    }
153
154    parts.join("; ")
155}
156
157/// Decode one numeric slot value: its real shell-namespace folder name when the
158/// ShellItem blob decodes, otherwise a size preview (never silently dropped).
159fn decode_slot(slot: &str, val: &winreg_core::value::Value<'_>) -> String {
160    if let Ok(raw) = val.raw_data() {
161        let items = shellitem::parse_idlist(&raw);
162        let path = shellitem::reconstruct_path(&items);
163        if !path.is_empty() {
164            return path;
165        }
166    }
167    let size = val.data_size() as usize;
168    format!("BagMRU[slot={slot}, size={size} bytes]")
169}