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    /// `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}