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 count_key = match hive.open_key(&count_path) {
79            Ok(Some(k)) => k,
80            _ => continue,
81        };
82
83        let values = match count_key.values() {
84            Ok(v) => v,
85            Err(_) => continue,
86        };
87
88        for val in values {
89            let raw = match val.raw_data() {
90                Ok(d) => d,
91                Err(_) => continue,
92            };
93
94            if raw.len() < UA_DATA_SIZE {
95                continue;
96            }
97
98            let run_count = winreg_core::bytes::le_u32(&raw[..], 4);
99            let focus_count = winreg_core::bytes::le_u32(&raw[..], 8);
100            let focus_duration_ms = winreg_core::bytes::le_u32(&raw[..], 12);
101            let filetime = winreg_core::bytes::le_u64(&raw[..], 60);
102
103            let last_run = filetime_to_datetime(filetime)
104                .map(|dt| dt.format("%Y-%m-%dT%H:%M:%SZ").to_string());
105
106            let program = rot13_decode(&val.name());
107
108            entries.push(UserAssistEntry {
109                program,
110                run_count,
111                focus_count,
112                focus_duration_ms,
113                last_run,
114                guid: guid.to_string(),
115            });
116        }
117    }
118
119    entries
120}
121
122// ── Unit tests ────────────────────────────────────────────────────────────────
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn rot13_roundtrip_hello() {
130        let s = "Hello, World!";
131        assert_eq!(rot13_decode(&rot13_decode(s)), s);
132    }
133
134    #[test]
135    fn rot13_numbers_unchanged() {
136        assert_eq!(rot13_decode("12345"), "12345");
137    }
138
139    #[test]
140    fn rot13_special_chars_unchanged() {
141        assert_eq!(rot13_decode("\\:{}[]()"), "\\:{}[]()");
142    }
143
144    #[test]
145    fn rot13_uppercase() {
146        assert_eq!(rot13_decode("HELLO"), "URYYB");
147    }
148
149    #[test]
150    fn rot13_lowercase() {
151        assert_eq!(rot13_decode("hello"), "uryyb");
152    }
153}