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}