usb_forensic/sources/
partition_diag.rs1use crate::{Attribute, Claim, DeviceKey, HistorySource, Provenance, SourceKind, Value};
13use winevt_extract::PartitionDiagEvent;
14
15pub struct PartitionDiagSource<'a> {
17 events: &'a [PartitionDiagEvent],
18}
19
20impl<'a> PartitionDiagSource<'a> {
21 #[must_use]
23 pub fn new(events: &'a [PartitionDiagEvent]) -> Self {
24 Self { events }
25 }
26}
27
28impl HistorySource for PartitionDiagSource<'_> {
29 fn claims(&self) -> Vec<Claim> {
30 let mut out = Vec::new();
31 for event in self.events {
32 push_event(event, &mut out);
33 }
34 out
35 }
36}
37
38fn device_key(event: &PartitionDiagEvent) -> Option<DeviceKey> {
42 event
43 .serial_number
44 .clone()
45 .filter(|s| !s.is_empty())
46 .or_else(|| event.disk_id.clone())
47 .map(DeviceKey)
48}
49
50fn push_event(event: &PartitionDiagEvent, out: &mut Vec<Claim>) {
51 let Some(device) = device_key(event) else {
52 return;
53 };
54 let locator = format!(
55 "Microsoft-Windows-Partition/Diagnostic#1006 DiskId={}",
56 event.disk_id.as_deref().unwrap_or("?")
57 );
58 let claim = |device, attribute, value| Claim {
59 device,
60 attribute,
61 value,
62 provenance: Provenance {
63 source: SourceKind::PartitionDiag,
64 locator: locator.clone(),
65 },
66 };
67 if let Ok(when) = event.timestamp.parse::<jiff::Timestamp>() {
70 out.push(claim(
71 device.clone(),
72 Attribute::LastConnected,
73 Value::Timestamp(when.as_second()),
74 ));
75 }
76 if let Some(serial) = volume_serial_string(event) {
77 out.push(claim(device, Attribute::VolumeSerial, Value::Text(serial)));
78 }
79}
80
81fn volume_serial_string(event: &PartitionDiagEvent) -> Option<String> {
87 if let Some(fat) = event.fat_volume_serial {
88 return Some(format!("{:04X}-{:04X}", fat >> 16, fat & 0xFFFF));
89 }
90 event.ntfs_volume_serial.map(|ntfs| {
91 format!(
92 "{:08X}-{:08X}",
93 (ntfs >> 32) as u32,
94 (ntfs & 0xFFFF_FFFF) as u32
95 )
96 })
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 fn event(serial: Option<&str>, disk_id: Option<&str>, ts: &str) -> PartitionDiagEvent {
104 PartitionDiagEvent {
105 timestamp: ts.to_string(),
106 event_id: 1006,
107 disk_number: Some(0),
108 bus_type: Some(7),
109 model: Some("Kingston DataTraveler".to_string()),
110 serial_number: serial.map(str::to_owned),
111 disk_id: disk_id.map(str::to_owned),
112 capacity: Some(1_000_000_000),
113 parent_id: Some("USB\\VID_0951&PID_1666\\SN123".to_string()),
114 vbr0_hex: None,
115 fat_volume_serial: None,
116 ntfs_volume_serial: None,
117 }
118 }
119
120 fn claims_for(e: PartitionDiagEvent) -> Vec<Claim> {
121 let evs = [e];
122 PartitionDiagSource::new(&evs).claims()
123 }
124
125 fn volume_serial_claim(claims: &[Claim]) -> Option<&Value> {
126 claims
127 .iter()
128 .find(|c| c.attribute == Attribute::VolumeSerial)
129 .map(|c| &c.value)
130 }
131
132 #[test]
133 fn fat_volume_serial_is_rendered_as_the_lnk_join_key() {
134 let mut e = event(Some("SN1"), None, "2020-01-01T00:00:00Z");
137 e.fat_volume_serial = Some(0xDEAD_BEEF);
138 let claims = claims_for(e);
139 assert_eq!(
140 volume_serial_claim(&claims),
141 Some(&Value::Text("DEAD-BEEF".to_string()))
142 );
143 }
144
145 #[test]
146 fn ntfs_volume_serial_is_rendered_as_a_distinct_8_byte_form() {
147 let mut e = event(Some("SN2"), None, "2020-01-01T00:00:00Z");
149 e.ntfs_volume_serial = Some(0x36B0_8F15_B08E_DAAF);
150 let claims = claims_for(e);
151 assert_eq!(
152 volume_serial_claim(&claims),
153 Some(&Value::Text("36B08F15-B08EDAAF".to_string()))
154 );
155 }
156
157 #[test]
158 fn no_volume_serial_emits_no_volume_serial_claim() {
159 let claims = claims_for(event(Some("SN3"), None, "2020-01-01T00:00:00Z"));
160 assert_eq!(volume_serial_claim(&claims), None);
161 }
162
163 #[test]
164 fn serial_keyed_event_yields_last_connected() {
165 let claims = claims_for(event(Some("SN123"), Some("{guid}"), "2020-01-01T00:00:00Z"));
166 assert_eq!(claims.len(), 1);
167 assert_eq!(claims[0].device, DeviceKey("SN123".to_string()));
168 assert_eq!(claims[0].attribute, Attribute::LastConnected);
169 assert_eq!(claims[0].value, Value::Timestamp(1_577_836_800));
170 assert_eq!(claims[0].provenance.source, SourceKind::PartitionDiag);
171 }
172
173 #[test]
174 fn empty_serial_falls_back_to_disk_id() {
175 let claims = claims_for(event(Some(""), Some("DISK-GUID-1"), "2020-01-01T00:00:00Z"));
177 assert_eq!(claims.len(), 1);
178 assert_eq!(claims[0].device, DeviceKey("DISK-GUID-1".to_string()));
179 }
180
181 #[test]
182 fn missing_serial_falls_back_to_disk_id() {
183 let claims = claims_for(event(None, Some("DISK-GUID-2"), "2020-01-01T00:00:00Z"));
184 assert_eq!(claims[0].device, DeviceKey("DISK-GUID-2".to_string()));
185 }
186
187 #[test]
188 fn no_identity_is_skipped() {
189 assert!(claims_for(event(Some(""), None, "2020-01-01T00:00:00Z")).is_empty());
191 }
192
193 #[test]
194 fn unparseable_timestamp_is_skipped() {
195 assert!(claims_for(event(Some("SN9"), None, "not-a-timestamp")).is_empty());
197 }
198
199 #[test]
200 fn subsecond_iso8601_truncates_to_whole_seconds() {
201 let claims = claims_for(event(Some("SN7"), None, "2020-08-01T21:47:55.7235131Z"));
203 assert_eq!(claims[0].value, Value::Timestamp(1_596_318_475));
204 }
205}