Skip to main content

usb_forensic/
source.rs

1//! The [`HistorySource`] contract: the stable integration point every reader-crate
2//! adapter implements.
3//!
4//! An adapter is *thin* — it maps a fleet reader crate's already-decoded output (a
5//! parsed registry value, a `setupapi` record, an event record) into atomic
6//! [`Claim`]s. It never parses raw artifact bytes itself; that is the reader crate's
7//! job, fuzzed and tested upstream. This keeps the adapter a pure, unit-testable
8//! mapping and keeps the correlation core source-agnostic.
9
10use crate::model::Claim;
11use crate::DeviceHistory;
12
13/// A source of USB-history claims — one thin adapter over one reader crate's output.
14pub trait HistorySource {
15    /// Emit every atomic claim this source can contribute.
16    fn claims(&self) -> Vec<Claim>;
17}
18
19/// Gather claims from many sources and correlate them into per-device histories — the
20/// one-call entry point a CLI or Issen uses.
21#[must_use]
22pub fn correlate_sources(sources: &[&dyn HistorySource]) -> Vec<DeviceHistory> {
23    let all: Vec<Claim> = sources.iter().flat_map(|s| s.claims()).collect();
24    // Attribute volume-keyed file access (LNK) to the device carrying that volume before
25    // grouping, so a device and the files touched on it land in one history.
26    let reconciled = crate::reconcile_volume_serials(&all);
27    crate::correlate(&reconciled)
28}
29
30#[cfg(test)]
31mod tests {
32    use super::*;
33    use crate::model::{Attribute, DeviceKey, Provenance, SourceKind, Value};
34    use crate::Consistency;
35
36    struct Mock(Vec<Claim>);
37    impl HistorySource for Mock {
38        fn claims(&self) -> Vec<Claim> {
39            self.0.clone()
40        }
41    }
42
43    fn claim(src: SourceKind) -> Claim {
44        Claim {
45            device: DeviceKey("SN1".into()),
46            attribute: Attribute::FirstConnected,
47            value: Value::Timestamp(1_700_000_000),
48            provenance: Provenance {
49                source: src,
50                locator: "x".into(),
51            },
52        }
53    }
54
55    #[test]
56    fn correlate_sources_merges_claims_from_every_source() {
57        let a = Mock(vec![claim(SourceKind::Usbstor)]);
58        let b = Mock(vec![claim(SourceKind::SetupApi)]);
59        let sources: [&dyn HistorySource; 2] = [&a, &b];
60        let hist = correlate_sources(&sources);
61        assert_eq!(hist.len(), 1);
62        // two distinct sources agreeing on the same value → corroborated
63        assert_eq!(hist[0].attributes[0].consistency, Consistency::Corroborated);
64    }
65}