Skip to main content

srum_parser/
lib.rs

1//! SRUDB.dat parser — reads SRUM records from ESE database files.
2//!
3//! SRUDB.dat is an ESE (JET Blue) database stored at
4//! `C:\Windows\System32\sru\SRUDB.dat`. On a live system it is locked;
5//! forensic analysis always operates on a copy.
6
7mod app_timeline;
8mod app_usage;
9mod connectivity;
10mod energy;
11mod energy_lt;
12mod id_map;
13mod network;
14mod push_notification;
15
16use ese_core::EseError;
17use forensicnomicon::srum::{
18    TABLE_APP_RESOURCE_USAGE, TABLE_APP_TIMELINE, TABLE_ENERGY_USAGE, TABLE_ENERGY_USAGE_LT,
19    TABLE_ID_MAP, TABLE_NETWORK_CONNECTIVITY, TABLE_NETWORK_USAGE, TABLE_PUSH_NOTIFICATIONS,
20};
21
22/// Errors produced by the SRUM parser.
23#[derive(Debug, thiserror::Error)]
24pub enum SrumError {
25    #[error("ese: {0}")]
26    Ese(#[from] EseError),
27    #[error("page {page} tag {tag}: {detail}")]
28    DecodeError {
29        page: u32,
30        tag: usize,
31        detail: String,
32    },
33}
34
35/// Iterate a named ESE table and decode each record, silently skipping
36/// records that fail either the ESE read or the domain decode step.
37///
38/// Returns `Ok(vec![])` when the table is absent — correct behaviour for
39/// Windows Server 2022 which omits several SRUM extensions entirely.
40fn collect_table<T>(
41    db: &ese_core::EseDatabase,
42    table: &str,
43    decode: impl Fn(&[u8], u32, usize) -> Result<T, SrumError>,
44) -> anyhow::Result<Vec<T>> {
45    let cursor = match db.table_records(table) {
46        Ok(c) => c,
47        Err(EseError::TableNotFound { .. }) => return Ok(vec![]),
48        Err(e) => return Err(e.into()),
49    };
50    let records = cursor
51        .filter_map(|r| match r {
52            Ok((page, tag, data)) => decode(&data, page, tag).ok(),
53            Err(_) => None,
54        })
55        .collect();
56    Ok(records)
57}
58
59/// Parse network usage records from a SRUDB.dat file.
60///
61/// Returns all records from the
62/// `{973F5D5C-1D90-4944-BE8E-24B94231A174}` table.
63///
64/// # Errors
65///
66/// Returns an error if the file cannot be read or is not a valid ESE database.
67pub fn parse_network_usage(
68    path: &std::path::Path,
69) -> anyhow::Result<Vec<srum_core::NetworkUsageRecord>> {
70    let db = ese_core::EseDatabase::open(path)?;
71    collect_table(&db, TABLE_NETWORK_USAGE, network::decode_network_record)
72}
73
74/// Parse application usage records from a SRUDB.dat file.
75///
76/// Returns all records from the
77/// `{5C8CF1C7-7257-4F13-B223-970EF5939312}` table.
78///
79/// # Errors
80///
81/// Returns an error if the file cannot be read or is not a valid ESE database.
82pub fn parse_app_usage(path: &std::path::Path) -> anyhow::Result<Vec<srum_core::AppUsageRecord>> {
83    let db = ese_core::EseDatabase::open(path)?;
84    collect_table(&db, TABLE_APP_RESOURCE_USAGE, app_usage::decode_app_record)
85}
86
87/// Parse network connectivity records from a SRUDB.dat file.
88///
89/// Returns all records from the
90/// `{DD6636C4-8929-4683-974E-22C046A43763}` table.
91///
92/// # Errors
93///
94/// Returns an error if the file cannot be read or is not a valid ESE database.
95pub fn parse_network_connectivity(
96    path: &std::path::Path,
97) -> anyhow::Result<Vec<srum_core::NetworkConnectivityRecord>> {
98    let db = ese_core::EseDatabase::open(path)?;
99    collect_table(
100        &db,
101        TABLE_NETWORK_CONNECTIVITY,
102        connectivity::decode_connectivity_record,
103    )
104}
105
106/// Parse energy usage records from a SRUDB.dat file.
107///
108/// Returns all records from the
109/// `{FEE4E14F-02A9-4550-B5CE-5FA2DA202E37}` table.
110///
111/// # Errors
112///
113/// Returns an error if the file cannot be read or is not a valid ESE database.
114pub fn parse_energy_usage(
115    path: &std::path::Path,
116) -> anyhow::Result<Vec<srum_core::EnergyUsageRecord>> {
117    let db = ese_core::EseDatabase::open(path)?;
118    collect_table(&db, TABLE_ENERGY_USAGE, energy::decode_energy_record)
119}
120
121/// Parse energy usage long-term records from a SRUDB.dat file.
122///
123/// Returns all records from the
124/// `{FEE4E14F-02A9-4550-B5CE-5FA2DA202E37}LT` table.
125///
126/// # Errors
127///
128/// Returns an error if the file cannot be read or is not a valid ESE database.
129pub fn parse_energy_lt(path: &std::path::Path) -> anyhow::Result<Vec<srum_core::EnergyLtRecord>> {
130    let db = ese_core::EseDatabase::open(path)?;
131    collect_table(
132        &db,
133        TABLE_ENERGY_USAGE_LT,
134        energy_lt::decode_energy_lt_record,
135    )
136}
137
138/// Parse push notification records from a SRUDB.dat file.
139///
140/// Returns all records from the
141/// `{D10CA2FE-6FCF-4F6D-848E-B2E99266FA89}` table.
142///
143/// # Errors
144///
145/// Returns an error if the file cannot be read or is not a valid ESE database.
146pub fn parse_push_notifications(
147    path: &std::path::Path,
148) -> anyhow::Result<Vec<srum_core::PushNotificationRecord>> {
149    let db = ese_core::EseDatabase::open(path)?;
150    collect_table(
151        &db,
152        TABLE_PUSH_NOTIFICATIONS,
153        push_notification::decode_push_notification_record,
154    )
155}
156
157/// Parse application timeline records from a SRUDB.dat file.
158///
159/// Returns all records from the
160/// `{7ACBBAA3-D029-4BE4-9A7A-0885927F1D8F}` table.
161///
162/// Available since Windows 10 Anniversary Update (1607).
163///
164/// # Errors
165///
166/// Returns an error if the file cannot be read or is not a valid ESE database.
167pub fn parse_app_timeline(
168    path: &std::path::Path,
169) -> anyhow::Result<Vec<srum_core::AppTimelineRecord>> {
170    let db = ese_core::EseDatabase::open(path)?;
171    collect_table(
172        &db,
173        TABLE_APP_TIMELINE,
174        app_timeline::decode_app_timeline_record,
175    )
176}
177
178/// Parse ID map entries from a SRUDB.dat file.
179///
180/// Returns all entries from the `SruDbIdMapTable` table.
181///
182/// # Errors
183///
184/// Returns an error if the file cannot be read or is not a valid ESE database.
185pub fn parse_id_map(path: &std::path::Path) -> anyhow::Result<Vec<srum_core::IdMapEntry>> {
186    let db = ese_core::EseDatabase::open(path)?;
187    collect_table(&db, TABLE_ID_MAP, |data, page, tag| {
188        id_map::decode_id_map_entry(data, page, tag)
189    })
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195    use tempfile::NamedTempFile;
196
197    #[test]
198    fn nonexistent_file_energy_lt_returns_err() {
199        let result = parse_energy_lt(std::path::Path::new("/nonexistent/SRUDB.dat"));
200        assert!(result.is_err());
201    }
202
203    #[test]
204    fn empty_file_energy_lt_returns_err() {
205        let tmp = NamedTempFile::new().expect("tempfile");
206        let result = parse_energy_lt(tmp.path());
207        assert!(result.is_err(), "empty file must return Err");
208    }
209
210    #[test]
211    fn energy_lt_result_type() {
212        let _: anyhow::Result<Vec<srum_core::EnergyLtRecord>> =
213            parse_energy_lt(std::path::Path::new("/nonexistent/SRUDB.dat"));
214    }
215
216    #[test]
217    fn nonexistent_file_app_timeline_returns_err() {
218        let result = parse_app_timeline(std::path::Path::new("/nonexistent/SRUDB.dat"));
219        assert!(result.is_err());
220    }
221
222    #[test]
223    fn empty_file_app_timeline_returns_err() {
224        let tmp = NamedTempFile::new().expect("tempfile");
225        let result = parse_app_timeline(tmp.path());
226        assert!(result.is_err(), "empty file must return Err");
227    }
228
229    #[test]
230    fn app_timeline_result_type() {
231        let _: anyhow::Result<Vec<srum_core::AppTimelineRecord>> =
232            parse_app_timeline(std::path::Path::new("/nonexistent/SRUDB.dat"));
233    }
234
235    #[test]
236    fn nonexistent_file_network_returns_err() {
237        let result = parse_network_usage(std::path::Path::new("/nonexistent/SRUDB.dat"));
238        assert!(result.is_err());
239    }
240
241    #[test]
242    fn nonexistent_file_app_returns_err() {
243        let result = parse_app_usage(std::path::Path::new("/nonexistent/SRUDB.dat"));
244        assert!(result.is_err());
245    }
246
247    #[test]
248    fn empty_file_network_returns_err() {
249        let tmp = NamedTempFile::new().expect("tempfile");
250        let result = parse_network_usage(tmp.path());
251        assert!(result.is_err(), "empty file must return Err");
252    }
253
254    #[test]
255    fn empty_file_app_returns_err() {
256        let tmp = NamedTempFile::new().expect("tempfile");
257        let result = parse_app_usage(tmp.path());
258        assert!(result.is_err(), "empty file must return Err");
259    }
260
261    #[test]
262    fn network_usage_result_type() {
263        let _: anyhow::Result<Vec<srum_core::NetworkUsageRecord>> =
264            parse_network_usage(std::path::Path::new("/nonexistent/SRUDB.dat"));
265    }
266
267    #[test]
268    fn app_usage_result_type() {
269        let _: anyhow::Result<Vec<srum_core::AppUsageRecord>> =
270            parse_app_usage(std::path::Path::new("/nonexistent/SRUDB.dat"));
271    }
272}