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