Skip to main content

winreg_artifacts/
amcache.rs

1//! Amcache registry artifact extractor.
2//!
3//! Amcache.hve records application execution evidence. This module decodes
4//! entries from `Root\InventoryApplicationFile`, each subkey representing a
5//! file that was executed or installed on the system.
6
7use std::io::Cursor;
8
9use winreg_core::hive::Hive;
10use winreg_core::key::filetime_to_datetime;
11
12// ---------------------------------------------------------------------------
13// Output type
14// ---------------------------------------------------------------------------
15
16/// A single file entry from `Root\InventoryApplicationFile`.
17#[derive(Debug, Clone, serde::Serialize)]
18pub struct AmcacheEntry {
19    /// Full lowercase file path (`LowerCaseLongPath`).
20    pub file_path: String,
21    /// SHA-1 hash: the `FileId` value with the leading `0000` prefix stripped.
22    /// Empty string if the value is absent.
23    pub sha1: String,
24    /// File size in bytes (`Size` as `REG_DWORD`).
25    pub size: u64,
26    /// PE link timestamp string, e.g. `"01/15/2023 10:30:00"` (`LinkDate`).
27    pub link_date: Option<String>,
28    /// Publisher name (`Publisher`).
29    pub publisher: String,
30    /// Product name (`ProductName`).
31    pub product_name: String,
32    /// Product version string (`ProductVersion`).
33    pub product_version: String,
34    /// Binary file version string (`BinFileVersion`).
35    pub bin_file_version: String,
36    /// The subkey name (hash identifier for this entry).
37    pub key_name: String,
38    /// Key `LastWriteTime` as ISO 8601, or `None` if unavailable.
39    pub last_written: Option<String>,
40}
41
42// ---------------------------------------------------------------------------
43// Key paths
44// ---------------------------------------------------------------------------
45
46/// Path to the InventoryApplicationFile container key (relative to hive root).
47const INVENTORY_APP_FILE: &str = "Root\\InventoryApplicationFile";
48
49// ---------------------------------------------------------------------------
50// Public parse function
51// ---------------------------------------------------------------------------
52
53/// Extract all `InventoryApplicationFile` entries from an Amcache hive.
54///
55/// Navigates `Root\InventoryApplicationFile`, iterates each subkey, and
56/// extracts the forensically relevant values. Missing values produce empty
57/// strings or zero rather than errors.
58///
59/// Returns an empty `Vec` if the key is not present.
60pub fn parse(hive: &Hive<Cursor<Vec<u8>>>) -> Vec<AmcacheEntry> {
61    let container = match hive.open_key(INVENTORY_APP_FILE) {
62        Ok(Some(k)) => k,
63        _ => return Vec::new(),
64    };
65
66    let subkeys = match container.subkeys() {
67        Ok(s) => s,
68        Err(_) => return Vec::new(),
69    };
70
71    let mut entries = Vec::with_capacity(subkeys.len());
72
73    for subkey in subkeys {
74        let key_name = subkey.name();
75
76        let last_written = filetime_to_datetime(subkey.last_written_raw())
77            .map(|dt| dt.format("%Y-%m-%dT%H:%M:%SZ").to_string());
78
79        // Helper: read a REG_SZ value, returning empty string on any error.
80        let read_sz = |name: &str| -> String {
81            subkey
82                .value(name)
83                .ok()
84                .flatten()
85                .and_then(|v| v.as_string().ok())
86                .unwrap_or_default()
87        };
88
89        let file_path = read_sz("LowerCaseLongPath");
90
91        // FileId: strip the leading "0000" prefix if present.
92        let file_id_raw = read_sz("FileId");
93        let sha1 = if file_id_raw.starts_with("0000") {
94            file_id_raw[4..].to_string()
95        } else {
96            file_id_raw
97        };
98
99        let size = subkey
100            .value("Size")
101            .ok()
102            .flatten()
103            .and_then(|v| v.as_u32().ok())
104            .unwrap_or(0) as u64;
105
106        let link_date_raw = read_sz("LinkDate");
107        let link_date = if link_date_raw.is_empty() {
108            None
109        } else {
110            Some(link_date_raw)
111        };
112
113        let publisher = read_sz("Publisher");
114        let product_name = read_sz("ProductName");
115        let product_version = read_sz("ProductVersion");
116        let bin_file_version = read_sz("BinFileVersion");
117
118        entries.push(AmcacheEntry {
119            file_path,
120            sha1,
121            size,
122            link_date,
123            publisher,
124            product_name,
125            product_version,
126            bin_file_version,
127            key_name,
128            last_written,
129        });
130    }
131
132    entries
133}