Skip to main content

zond_engine/core/models/host/
hardware.rs

1// Copyright (c) 2026 Erik Lening (hollowpointer) and Contributors
2//
3// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
4// If a copy of the MPL was not distributed with this file, You can obtain one at
5// https://mozilla.org/MPL/2.0/.
6
7//! # Hardware Information
8//!
9//! This module defines the [`HardwareInfo`] model for identifying physical
10//! network characteristics. It records hardware addresses (MACs) and vendor
11//! metadata derived from the Organiztionally Unique Identifier (OUI).
12//!
13//! The model handles modern network complexities such as MAC randomization
14//! and multi-homed hosts by tracking a history of all seen addresses.
15
16use crate::core::models::mac::{self, MacAddr};
17use std::{collections::BTreeMap, sync::Arc, time::Instant};
18
19/// Physical hardware identification and auditing data for a network host.
20///
21/// `HardwareInfo` tracks multiple MAC addresses to support multi-NIC hosts
22/// and to detect "MAC hopping" on randomized devices.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct HardwareInfo {
25    /// Discovered MAC addresses and the last time they were observed.
26    pub(crate) macs: BTreeMap<MacAddr, Instant>,
27
28    /// The hardware vendor identified from the MAC OUI (e.g., "Apple", "Dell").
29    ///
30    /// This field should ideally be shared via an `Arc` to minimize heap
31    /// allocations across thousands of identical host records.
32    pub vendor: Option<Arc<str>>,
33}
34
35impl HardwareInfo {
36    /// Creates a new `HardwareInfo` record for a specifically discovered MAC address.
37    ///
38    /// The vendor is resolved automatically from the MAC's OUI, if known.
39    pub fn new(mac: MacAddr) -> Self {
40        let mut macs = BTreeMap::new();
41        macs.insert(mac, Instant::now());
42
43        Self {
44            macs,
45            vendor: mac::vendor(&mac).map(Arc::from),
46        }
47    }
48
49    /// Records a discovery event for a specific MAC address, updating its
50    /// "last seen" timestamp.
51    ///
52    /// If no vendor has been identified yet, this attempts to resolve one
53    /// from the newly observed MAC's OUI.
54    pub fn add_mac(&mut self, mac: MacAddr) {
55        self.macs.insert(mac, Instant::now());
56
57        if self.vendor.is_none() {
58            self.vendor = mac::vendor(&mac).map(Arc::from);
59        }
60    }
61
62    /// Returns a read-only view of all recorded MAC addresses and their
63    /// last-seen timestamps.
64    #[inline]
65    pub fn macs(&self) -> &BTreeMap<MacAddr, Instant> {
66        &self.macs
67    }
68
69    /// Returns the MAC address that was most recently observed.
70    ///
71    /// This is typically used to identify the primary hardware interface
72    /// currently active on the network.
73    pub fn most_recent_mac(&self) -> Option<MacAddr> {
74        self.macs
75            .iter()
76            .max_by_key(|&(_, time)| time)
77            .map(|(mac, _)| *mac)
78    }
79
80    /// Removes all MAC address records that were last seen before the given `cutoff`.
81    ///
82    /// This is a critical forensic cleanup step for long-running monitors in
83    /// environments with aggressive MAC randomization, preventing memory
84    /// exhaustion from "ghost" hardware records.
85    pub fn prune_stale_macs(&mut self, cutoff: Instant) {
86        self.macs.retain(|_, last_seen| *last_seen >= cutoff);
87    }
88
89    /// Merges architectural findings from another hardware record.
90    ///
91    /// MAC addresses are interleaved, with the newest timestamp prevailing
92    /// for each unique address to prevent timeline regressions.
93    pub fn merge(&mut self, other: HardwareInfo) {
94        for (mac, time) in other.macs {
95            self.macs
96                .entry(mac)
97                .and_modify(|t| {
98                    if time > *t {
99                        *t = time;
100                    }
101                })
102                .or_insert(time);
103        }
104
105        if self.vendor.is_none() {
106            self.vendor = other.vendor;
107        }
108    }
109}
110
111// ╔════════════════════════════════════════════╗
112// ║ ████████╗███████╗███████╗████████╗███████╗ ║
113// ║ ╚══██╔══╝██╔════╝██╔════╝╚══██╔══╝██╔════╝ ║
114// ║    ██║   █████╗  ███████╗   ██║   ███████╗ ║
115// ║    ██║   ██╔══╝  ╚════██║   ██║   ╚════██║ ║
116// ║    ██║   ███████╗███████║   ██║   ███████║ ║
117// ║    ╚═╝   ╚══════╝╚══════╝   ╚═╝   ╚══════╝ ║
118// ╚════════════════════════════════════════════╝
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use std::time::Duration;
124
125    #[test]
126    fn hardware_vendor_assignment() {
127        let mac = MacAddr::new(0x00, 0x0C, 0x29, 0xAB, 0xCD, 0xEF);
128        let mut hw = HardwareInfo::new(mac);
129        hw.vendor = Some(Arc::from("VMware, Inc."));
130
131        assert_eq!(hw.vendor.as_deref().unwrap(), "VMware, Inc.");
132        assert!(hw.macs().contains_key(&mac));
133    }
134
135    #[test]
136    fn test_most_recent_mac_selection() {
137        let mac_old = MacAddr::new(1, 1, 1, 1, 1, 1);
138        let mac_new = MacAddr::new(2, 2, 2, 2, 2, 2);
139
140        let mut hw = HardwareInfo::new(mac_old);
141        let future_time = Instant::now() + Duration::from_secs(60);
142        hw.macs.insert(mac_new, future_time);
143
144        assert_eq!(hw.most_recent_mac(), Some(mac_new));
145    }
146
147    #[test]
148    fn most_recent_mac_on_empty() {
149        // Construct an empty one manually via the pub(crate) field
150        let hw = HardwareInfo {
151            macs: BTreeMap::new(),
152            vendor: None,
153        };
154        assert_eq!(hw.most_recent_mac(), None);
155    }
156
157    #[test]
158    fn prune_stale_macs_logic() {
159        let mac_keep = MacAddr::new(1, 1, 1, 1, 1, 1);
160        let mac_drop = MacAddr::new(2, 2, 2, 2, 2, 2);
161
162        let mut hw = HardwareInfo::new(mac_keep);
163        let past_time = Instant::now() - Duration::from_secs(3600);
164        hw.macs.insert(mac_drop, past_time);
165
166        let cutoff = Instant::now() - Duration::from_secs(1800);
167        hw.prune_stale_macs(cutoff);
168
169        assert_eq!(hw.macs().len(), 1);
170        assert!(hw.macs().contains_key(&mac_keep));
171    }
172}