Skip to main content

winreg_artifacts/
userassist.rs

1//! `UserAssist` registry artifact extractor.
2//!
3//! Windows stores program launch counts and last-run timestamps in
4//! `Software\Microsoft\Windows\CurrentVersion\Explorer\UserAssist\{GUID}\Count`
5//! in NTUSER.DAT hives. Value names are ROT13-encoded paths; value data is a
6//! 72-byte binary struct with run count, focus info, and a FILETIME.
7
8use std::io::Cursor;
9
10use winreg_core::hive::Hive;
11use winreg_core::key::filetime_to_datetime;
12
13// ── Well-known UserAssist GUIDs ───────────────────────────────────────────────
14
15/// Win7+ executable stats GUID.
16const GUID_EXE: &str = "{CEBFF5CD-ACE2-4F4F-9178-9926F41749EA}";
17
18/// Win7+ shortcut (.lnk) stats GUID.
19const GUID_LNK: &str = "{F4E57C4B-2036-45F0-A9AB-443BCFE33D9F}";
20
21/// All GUIDs to enumerate.
22const KNOWN_GUIDS: &[&str] = &[GUID_EXE, GUID_LNK];
23
24// ── Binary value layout ───────────────────────────────────────────────────────
25
26/// Minimum data size for a valid `UserAssist` binary value.
27const UA_DATA_SIZE: usize = 68; // bytes 60-67 (FILETIME) must be accessible
28
29// ── Output type ───────────────────────────────────────────────────────────────
30
31/// A `UserAssist` entry from the registry.
32#[derive(Debug, Clone, serde::Serialize)]
33pub struct UserAssistEntry {
34    /// ROT13-decoded program path / name.
35    pub program: String,
36    /// Raw run count from bytes 4-7 of the binary value data.
37    pub run_count: u32,
38    /// Focus count from bytes 8-11.
39    pub focus_count: u32,
40    /// Focus duration in milliseconds from bytes 12-15.
41    pub focus_duration_ms: u32,
42    /// ISO 8601 last-run timestamp from FILETIME at bytes 60-67, or `None` if zero.
43    pub last_run: Option<String>,
44    /// The GUID subkey this entry came from.
45    pub guid: String,
46}
47
48// ── ROT13 decode ──────────────────────────────────────────────────────────────
49
50/// ROT13-decode a string: rotate A-Z and a-z by 13, leave other chars unchanged.
51pub fn rot13_decode(s: &str) -> String {
52    s.chars()
53        .map(|c| match c {
54            'A'..='Z' => (b'A' + (c as u8 - b'A' + 13) % 26) as char,
55            'a'..='z' => (b'a' + (c as u8 - b'a' + 13) % 26) as char,
56            other => other,
57        })
58        .collect()
59}
60
61// ── Public parse function ─────────────────────────────────────────────────────
62
63/// Extract all `UserAssist` entries from an NTUSER.DAT hive.
64///
65/// Enumerates both the executable and shortcut GUID subkeys under
66/// `Software\Microsoft\Windows\CurrentVersion\Explorer\UserAssist\{GUID}\Count`,
67/// ROT13-decodes each value name, and parses the binary payload.
68///
69/// Returns an empty Vec if no `UserAssist` keys are present.
70pub fn parse(hive: &Hive<Cursor<Vec<u8>>>) -> Vec<UserAssistEntry> {
71    let mut entries = Vec::new();
72
73    for &guid in KNOWN_GUIDS {
74        let count_path = format!(
75            "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\UserAssist\\{guid}\\Count"
76        );
77
78        let Ok(Some(count_key)) = hive.open_key(&count_path) else {
79            continue;
80        };
81
82        let Ok(values) = count_key.values() else {
83            continue;
84        };
85
86        for val in values {
87            let Ok(raw) = val.raw_data() else {
88                continue;
89            };
90
91            if raw.len() < UA_DATA_SIZE {
92                continue;
93            }
94
95            let run_count = winreg_core::bytes::le_u32(&raw[..], 4);
96            let focus_count = winreg_core::bytes::le_u32(&raw[..], 8);
97            let focus_duration_ms = winreg_core::bytes::le_u32(&raw[..], 12);
98            let filetime = winreg_core::bytes::le_u64(&raw[..], 60);
99
100            let last_run = filetime_to_datetime(filetime)
101                .map(|dt| dt.format("%Y-%m-%dT%H:%M:%SZ").to_string());
102
103            let program = rot13_decode(&val.name());
104
105            entries.push(UserAssistEntry {
106                program,
107                run_count,
108                focus_count,
109                focus_duration_ms,
110                last_run,
111                guid: guid.to_string(),
112            });
113        }
114    }
115
116    entries
117}
118
119// ── Unit tests ────────────────────────────────────────────────────────────────
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn rot13_roundtrip_hello() {
127        let s = "Hello, World!";
128        assert_eq!(rot13_decode(&rot13_decode(s)), s);
129    }
130
131    #[test]
132    fn rot13_numbers_unchanged() {
133        assert_eq!(rot13_decode("12345"), "12345");
134    }
135
136    #[test]
137    fn rot13_special_chars_unchanged() {
138        assert_eq!(rot13_decode("\\:{}[]()"), "\\:{}[]()");
139    }
140
141    #[test]
142    fn rot13_uppercase() {
143        assert_eq!(rot13_decode("HELLO"), "URYYB");
144    }
145
146    #[test]
147    fn rot13_lowercase() {
148        assert_eq!(rot13_decode("hello"), "uryyb");
149    }
150}