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