Skip to main content

usb_forensic/
model.rs

1//! The source-agnostic domain model: atomic claims a source adapter emits, which the
2//! correlation core groups and grades.
3//!
4//! Enum variants are intentionally minimal and `#[non_exhaustive]` — each new source
5//! adds the variant it needs (additive, non-breaking). The full planned set is the
6//! `docs/feature-parity.md` checklist.
7
8use serde::Serialize;
9
10/// Which artifact a claim was extracted from.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
12#[non_exhaustive]
13pub enum SourceKind {
14    /// `SYSTEM\...\Enum\{USBSTOR,SCSI,USB}` — device instance keys and their
15    /// install / first-install / last-arrival / last-removal property `FILETIME`s.
16    Usbstor,
17    /// `SYSTEM\MountedDevices` — drive-letter ↔ device mapping.
18    MountedDevices,
19    /// `setupapi.dev.log` — first device-install time.
20    SetupApi,
21    /// Microsoft-Windows-Partition/Diagnostic event log — volume serials.
22    PartitionDiag,
23    /// Microsoft-Windows-Kernel-PnP/Configuration event log — USB device-configuration
24    /// events (a connection witness keyed by the device instance serial).
25    KernelPnp,
26    /// Microsoft-Windows-DriverFrameworks-UserMode/Operational event log — UMDF device
27    /// arrival (EID 2003, connect) and final removal (EID 2102, disconnect), keyed by the
28    /// device instance serial. Disabled by default on Win8+, so present only when enabled.
29    DriverFramework,
30    /// A Windows Shell Link (`.lnk`) — the volume-serial file join.
31    Lnk,
32    /// A Windows Jump List (`*.automaticDestinations-ms` / `*.customDestinations-ms`).
33    JumpList,
34    /// A Linux kernel log (`syslog` / `dmesg`) — USB enumeration events.
35    LinuxKernelLog,
36    /// `SOFTWARE\...\Windows Search\VolumeInfoCache` — cached volume labels per drive.
37    VolumeInfoCache,
38    /// `NTUSER\...\Explorer\MountPoints2` — per-user volume mounts (by volume GUID).
39    MountPoints2,
40    /// `NTUSER`/`UsrClass` `...\Shell\BagMRU` (ShellBags) — per-user folders browsed
41    /// in Explorer, keyed by the drive letter the folder lived on.
42    Shellbag,
43    /// `SOFTWARE\...\EMDMgmt` — the `ReadyBoost` cache: volume label + serial history.
44    EmdMgmt,
45    /// A raw disk image of a physical device — its MBR/VBR boot sectors.
46    DeviceImage,
47    /// macOS `com.apple.iPod.plist` — Apple-device (iPhone/iPad/iPod) connection history.
48    AppleIPod,
49    /// macOS `system_profiler` / `IORegistry` — the live USB device tree.
50    MacosUsb,
51    /// macOS unified log — USB enumeration (connect) events with times.
52    MacosUnifiedLog,
53}
54
55/// The physical storage container an artifact lives in — the tamper surface.
56///
57/// Corroboration counts *independent* sources, and independence is a property of the
58/// container, not the recording mechanism: two sources in the same container share one
59/// tamper surface, so their agreement is not tamper-independent. Distinct from
60/// [`SourceKind`], which is the recording mechanism (guards against parse error and
61/// coincidence, a weaker form of independence).
62#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
63#[non_exhaustive]
64pub enum ArtifactContainer {
65    /// The `SYSTEM` registry hive (USBSTOR, MountedDevices, …).
66    SystemHive,
67    /// The `setupapi.dev.log` text log.
68    SetupApiLog,
69    /// A Windows event log (`.evtx`).
70    EventLog,
71    /// A Shell Link file (`.lnk`) or jump list on the filesystem.
72    LnkFile,
73    /// A Linux kernel log file (`syslog` / `dmesg`).
74    KernelLog,
75    /// The `SOFTWARE` registry hive (VolumeInfoCache, WPD, …) — a file distinct from the
76    /// `SYSTEM` hive, so a separate tamper surface.
77    SoftwareHive,
78    /// A per-user `NTUSER.DAT` hive (MountPoints2, …) — a distinct per-user tamper surface.
79    UserHive,
80    /// The physical device's own media (MBR/VBR boot sectors) — the strongest surface.
81    DeviceMedia,
82    /// A macOS preferences/property-list artifact (com.apple.iPod.plist, …).
83    MacosPlist,
84}
85
86impl SourceKind {
87    /// The storage container this source lives in — its tamper surface. Total.
88    #[must_use]
89    pub const fn container(self) -> ArtifactContainer {
90        match self {
91            Self::Usbstor | Self::MountedDevices => ArtifactContainer::SystemHive,
92            Self::SetupApi => ArtifactContainer::SetupApiLog,
93            Self::PartitionDiag | Self::KernelPnp | Self::DriverFramework => {
94                ArtifactContainer::EventLog
95            }
96            Self::Lnk | Self::JumpList => ArtifactContainer::LnkFile,
97            Self::LinuxKernelLog => ArtifactContainer::KernelLog,
98            Self::VolumeInfoCache | Self::EmdMgmt => ArtifactContainer::SoftwareHive,
99            Self::MountPoints2 | Self::Shellbag => ArtifactContainer::UserHive,
100            Self::DeviceImage => ArtifactContainer::DeviceMedia,
101            Self::AppleIPod | Self::MacosUsb | Self::MacosUnifiedLog => {
102                ArtifactContainer::MacosPlist
103            }
104        }
105    }
106
107    /// Whether this source records timestamps in **host-local** time (rather than UTC).
108    ///
109    /// `setupapi.dev.log` and Linux kernel logs write local wall-clock with no zone, so
110    /// their readers convert them naively (local-as-UTC). Registry `FILETIME`, event-log
111    /// `FILETIME`, and LNK/jump-list epochs are true UTC. [`normalize_local_clocks`]
112    /// uses this to correct local timestamps to UTC given the host's offset.
113    ///
114    /// [`normalize_local_clocks`]: crate::normalize_local_clocks
115    #[must_use]
116    pub fn clock_is_local(self) -> bool {
117        matches!(self, Self::SetupApi | Self::LinuxKernelLog)
118    }
119}
120
121/// Which device attribute a claim describes.
122#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
123#[non_exhaustive]
124pub enum Attribute {
125    /// First time the device was connected to this system.
126    FirstConnected,
127    /// Most recent time the device was connected.
128    LastConnected,
129    /// Most recent time the device was removed (disconnected).
130    LastRemoved,
131    /// Volume label (friendly name) of the device's volume.
132    VolumeName,
133    /// Volume serial number of the device's volume.
134    VolumeSerial,
135    /// A file accessed from the device (e.g. an LNK target) — the file-to-device link.
136    AccessedFile,
137    /// A directory browsed on the device in Explorer (a ShellBags `BagMRU` folder) —
138    /// corroborates the volume was mounted and names a directory touched on it.
139    /// Distinct from [`AccessedFile`](Self::AccessedFile): a folder browsed, not a
140    /// file opened.
141    BrowsedFolder,
142    /// A drive letter the device's volume was mounted as (e.g. `E:`), from the
143    /// `MountedDevices` drive-letter↔device join.
144    DriveLetter,
145    /// The volume's encryption type (e.g. `BitLocker`), detected from its boot sector.
146    Encryption,
147    /// The device class/protocol when notable (e.g. `MTP` for a phone/tablet/camera).
148    DeviceClass,
149}
150
151/// A comparable claim value, normalized by the source adapter.
152///
153/// Timestamps are epoch **seconds, UTC**: the adapter normalizes each source's native
154/// precision (registry `FILETIME` is 100 ns, `setupapi` is 1 s) down to seconds so the
155/// core compares like-for-like. Sub-second precision is not a real disagreement, so it
156/// is removed at the boundary rather than papered over with a tolerance constant here.
157#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
158pub enum Value {
159    /// A point in time, epoch seconds UTC.
160    Timestamp(i64),
161    /// A textual value (volume name, serial, …), verbatim.
162    Text(String),
163}
164
165/// Where a value came from: the source plus a locator (registry key path, log line,
166/// event record id). The reproducibility chain (raw bytes → decoding rule) extends this.
167#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
168pub struct Provenance {
169    /// The artifact the value was read from.
170    pub source: SourceKind,
171    /// A precise pointer within that artifact (e.g. the full key path or log line).
172    pub locator: String,
173}
174
175/// Cross-source identity of a device — typically the device/instance serial number
176/// that appears across `USBSTOR`, `MountedDevices`, `setupapi`, and the event log.
177#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
178pub struct DeviceKey(pub String);
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn linux_kernel_log_is_its_own_container_with_a_local_clock() {
186        // A Linux syslog/dmesg file is a distinct tamper surface from any Windows
187        // artifact, and (like setupapi) records host-local wall-clock.
188        assert_eq!(
189            SourceKind::LinuxKernelLog.container(),
190            ArtifactContainer::KernelLog
191        );
192        assert!(SourceKind::LinuxKernelLog.clock_is_local());
193    }
194
195    #[test]
196    fn registry_source_lives_in_the_system_hive_container_in_utc() {
197        assert_eq!(
198            SourceKind::Usbstor.container(),
199            ArtifactContainer::SystemHive
200        );
201        assert!(!SourceKind::Usbstor.clock_is_local());
202    }
203
204    #[test]
205    fn volume_info_cache_is_the_software_hive_a_distinct_container_from_system() {
206        // SOFTWARE and SYSTEM are separate files → separate tamper surfaces, so a
207        // VolumeInfoCache label corroborating a SYSTEM device is cross-container.
208        assert_eq!(
209            SourceKind::VolumeInfoCache.container(),
210            ArtifactContainer::SoftwareHive
211        );
212        assert_ne!(
213            SourceKind::VolumeInfoCache.container(),
214            SourceKind::Usbstor.container()
215        );
216        assert!(!SourceKind::VolumeInfoCache.clock_is_local());
217    }
218
219    #[test]
220    fn mountpoints2_is_a_per_user_hive_container() {
221        // NTUSER.DAT is per-user — distinct from SYSTEM and SOFTWARE, so a per-user mount
222        // corroborating a machine-wide device is cross-container.
223        assert_eq!(
224            SourceKind::MountPoints2.container(),
225            ArtifactContainer::UserHive
226        );
227        assert_ne!(
228            SourceKind::MountPoints2.container(),
229            SourceKind::VolumeInfoCache.container()
230        );
231        assert!(!SourceKind::MountPoints2.clock_is_local());
232    }
233
234    #[test]
235    fn emdmgmt_shares_the_software_hive_container() {
236        assert_eq!(
237            SourceKind::EmdMgmt.container(),
238            ArtifactContainer::SoftwareHive
239        );
240    }
241
242    #[test]
243    fn macos_unified_log_shares_the_macos_artifact_container() {
244        assert_eq!(
245            SourceKind::MacosUnifiedLog.container(),
246            ArtifactContainer::MacosPlist
247        );
248        assert!(!SourceKind::MacosUnifiedLog.clock_is_local());
249    }
250
251    #[test]
252    fn macos_usb_shares_the_macos_artifact_container() {
253        assert_eq!(
254            SourceKind::MacosUsb.container(),
255            ArtifactContainer::MacosPlist
256        );
257        assert!(!SourceKind::MacosUsb.clock_is_local());
258    }
259
260    #[test]
261    fn apple_ipod_is_a_macos_plist_container() {
262        // A macOS plist is a distinct tamper surface from any Windows/Linux artifact.
263        assert_eq!(
264            SourceKind::AppleIPod.container(),
265            ArtifactContainer::MacosPlist
266        );
267        assert!(!SourceKind::AppleIPod.clock_is_local());
268    }
269
270    #[test]
271    fn device_image_is_its_own_device_media_container() {
272        // The physical device's own boot sectors — the strongest, distinct tamper surface.
273        assert_eq!(
274            SourceKind::DeviceImage.container(),
275            ArtifactContainer::DeviceMedia
276        );
277    }
278}
279
280/// One atomic extracted fact about one device, from one source.
281#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
282pub struct Claim {
283    /// The device this fact is about.
284    pub device: DeviceKey,
285    /// The attribute this fact describes.
286    pub attribute: Attribute,
287    /// The value the source reported.
288    pub value: Value,
289    /// Where the value came from.
290    pub provenance: Provenance,
291}