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