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}