1use crate::core::models::port::Port;
18use std::{
19 collections::{BTreeMap, BTreeSet, HashMap, HashSet},
20 net::IpAddr,
21 time::SystemTime,
22};
23
24pub mod hardware;
25pub mod os;
26pub mod status;
27pub mod telemetry;
28
29pub use hardware::HardwareInfo;
30pub use os::OsFingerprint;
31pub use status::{HostStatus, StatusProtocol, StatusReason};
32pub use telemetry::HostTelemetry;
33
34pub const MAX_PORTS_PER_HOST: usize = 1000;
40
41#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
43pub enum NetworkRole {
44 Gateway,
46 DHCP,
48 DNS,
50 Tarpit,
53}
54
55#[derive(Debug, Clone)]
64pub struct Host {
65 primary_ip: IpAddr,
67
68 ips: BTreeSet<IpAddr>,
70
71 hostname: Option<String>,
73
74 status: HostStatus,
76
77 reasons: HashSet<StatusReason>,
79
80 os: Option<Box<OsFingerprint>>,
82
83 hardware: Option<HardwareInfo>,
85
86 telemetry: HostTelemetry,
88
89 network_roles: HashSet<NetworkRole>,
91
92 scripts: Option<HashMap<String, String>>,
94
95 first_seen: SystemTime,
97
98 last_seen: SystemTime,
100
101 ports: BTreeMap<u16, Port>,
103}
104
105impl Host {
106 pub fn new(primary_ip: IpAddr) -> Self {
110 let mut ips = BTreeSet::new();
111 ips.insert(primary_ip);
112 let now = SystemTime::now();
113
114 Self {
115 primary_ip,
116 ips,
117 hostname: None,
118 status: HostStatus::Unknown,
119 reasons: HashSet::new(),
120 os: None,
121 hardware: None,
122 telemetry: HostTelemetry::default(),
123 network_roles: HashSet::new(),
124 scripts: None,
125 first_seen: now,
126 last_seen: now,
127 ports: BTreeMap::new(),
128 }
129 }
130
131 pub fn primary_ip(&self) -> IpAddr {
133 self.primary_ip
134 }
135
136 pub fn ips(&self) -> &BTreeSet<IpAddr> {
138 &self.ips
139 }
140
141 pub fn hostname(&self) -> Option<&str> {
143 self.hostname.as_deref()
144 }
145
146 pub fn status(&self) -> HostStatus {
148 self.status
149 }
150
151 pub fn reasons(&self) -> &HashSet<StatusReason> {
153 &self.reasons
154 }
155
156 pub fn os(&self) -> Option<&OsFingerprint> {
158 self.os.as_deref()
159 }
160
161 pub fn hardware(&self) -> Option<&HardwareInfo> {
163 self.hardware.as_ref()
164 }
165
166 pub fn telemetry(&self) -> &HostTelemetry {
168 &self.telemetry
169 }
170
171 pub fn network_roles(&self) -> &HashSet<NetworkRole> {
173 &self.network_roles
174 }
175
176 pub fn scripts(&self) -> Option<&HashMap<String, String>> {
178 self.scripts.as_ref()
179 }
180
181 pub fn first_seen(&self) -> SystemTime {
183 self.first_seen
184 }
185
186 pub fn last_seen(&self) -> SystemTime {
188 self.last_seen
189 }
190
191 pub fn set_primary_ip(&mut self, ip: IpAddr) {
194 self.primary_ip = ip;
195 self.ips.insert(ip);
196 self.last_seen = SystemTime::now();
197 }
198
199 pub fn add_ip(&mut self, ip: IpAddr) -> bool {
202 let is_new = self.ips.insert(ip);
203 self.last_seen = SystemTime::now();
204 is_new
205 }
206
207 pub fn extend_ips(&mut self, ips: impl IntoIterator<Item = IpAddr>) {
209 self.ips.extend(ips);
210 self.last_seen = SystemTime::now();
211 }
212
213 pub fn set_hostname(&mut self, hostname: Option<String>) {
215 self.hostname = hostname;
216 self.last_seen = SystemTime::now();
217 }
218
219 pub fn set_status(&mut self, status: HostStatus) {
221 self.status = status;
222 self.last_seen = SystemTime::now();
223 }
224
225 pub fn add_reason(&mut self, reason: StatusReason) {
227 self.reasons.insert(reason);
228 self.last_seen = SystemTime::now();
229 }
230
231 pub fn set_os(&mut self, os: OsFingerprint) {
233 self.os = Some(Box::new(os));
234 self.last_seen = SystemTime::now();
235 }
236
237 pub fn set_hardware(&mut self, hardware: HardwareInfo) {
239 self.hardware = Some(hardware);
240 self.last_seen = SystemTime::now();
241 }
242
243 pub fn with_mac(mut self, mac: crate::core::models::mac::MacAddr) -> Self {
245 self.set_hardware(HardwareInfo::new(mac));
246 self
247 }
248
249 pub fn add_rtt(&mut self, rtt: std::time::Duration) {
251 self.telemetry.add_rtt(rtt);
252 self.last_seen = SystemTime::now();
253 }
254
255 pub fn with_rtt(mut self, rtt: std::time::Duration) -> Self {
257 self.add_rtt(rtt);
258 self
259 }
260
261 pub fn set_rtts(&mut self, rtts: impl IntoIterator<Item = std::time::Duration>) {
263 for rtt in rtts {
264 self.telemetry.add_rtt(rtt);
265 }
266 self.last_seen = SystemTime::now();
267 }
268
269 pub fn add_network_role(&mut self, role: NetworkRole) {
271 self.network_roles.insert(role);
272 self.last_seen = SystemTime::now();
273 }
274
275 pub fn add_script_result(&mut self, key: String, value: String) {
277 self.scripts
278 .get_or_insert_with(HashMap::new)
279 .insert(key, value);
280 self.last_seen = SystemTime::now();
281 }
282
283 pub fn min_rtt(&self) -> Option<std::time::Duration> {
285 self.telemetry.min_rtt()
286 }
287
288 pub fn max_rtt(&self) -> Option<std::time::Duration> {
290 self.telemetry.max_rtt()
291 }
292
293 pub fn average_rtt(&self) -> Option<std::time::Duration> {
295 self.telemetry.average_rtt()
296 }
297
298 pub fn mac(&self) -> Option<crate::core::models::mac::MacAddr> {
300 self.hardware.as_ref().and_then(|h| h.most_recent_mac())
301 }
302
303 pub fn vendor(&self) -> Option<&str> {
305 self.hardware
306 .as_ref()
307 .and_then(|h| h.vendor.as_ref())
308 .map(|v| &**v)
309 }
310
311 pub fn is_alive(&self) -> bool {
314 self.status.is_alive()
315 }
316
317 pub fn ports(&self) -> impl Iterator<Item = &Port> {
319 self.ports.values()
320 }
321
322 pub fn port_count(&self) -> usize {
324 self.ports.len()
325 }
326
327 pub fn add_port(&mut self, new_port: Port) {
333 if self.ports.len() >= MAX_PORTS_PER_HOST && !self.ports.contains_key(&new_port.number()) {
334 self.network_roles.insert(NetworkRole::Tarpit);
335 return;
336 }
337
338 self.ports
339 .entry(new_port.number())
340 .and_modify(|p| p.merge(new_port.clone()))
341 .or_insert(new_port);
342
343 self.last_seen = SystemTime::now();
344 }
345
346 pub fn merge(&mut self, other: Host) {
355 self.ips.extend(other.ips);
356 if self.hostname.is_none() {
357 self.hostname = other.hostname;
358 }
359
360 if other.status > self.status {
361 self.status = other.status;
362 }
363 self.reasons.extend(other.reasons);
364
365 if let Some(other_os) = other.os {
366 if let Some(ref mut self_os) = self.os {
367 self_os.merge(*other_os);
368 } else {
369 self.os = Some(other_os);
370 }
371 }
372
373 if let Some(other_hw) = other.hardware {
374 if let Some(ref mut self_hw) = self.hardware {
375 self_hw.merge(other_hw);
376 } else {
377 self.hardware = Some(other_hw);
378 }
379 }
380
381 self.telemetry.merge(other.telemetry);
382 self.network_roles.extend(other.network_roles);
383
384 if let Some(other_scripts) = other.scripts {
385 let self_scripts = self.scripts.get_or_insert_with(HashMap::new);
386 self_scripts.extend(other_scripts);
387 }
388
389 for (_, port) in other.ports {
390 self.add_port(port);
391 }
392
393 if other.first_seen < self.first_seen {
394 self.first_seen = other.first_seen;
395 }
396 if other.last_seen > self.last_seen {
397 self.last_seen = other.last_seen;
398 }
399 }
400}
401
402impl std::fmt::Display for Host {
403 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
404 write!(f, "{} ({})", self.primary_ip, self.status)?;
405 if let Some(ref os) = self.os {
406 write!(f, " - {}", os)?;
407 }
408 if self.network_roles.contains(&NetworkRole::Tarpit) {
409 write!(f, " [TARPIT]")?;
410 } else {
411 write!(f, " [{}]", self.telemetry)?;
412 }
413 Ok(())
414 }
415}
416
417#[cfg(test)]
427mod tests {
428 use super::*;
429 use crate::core::models::port::{Port, PortState, Protocol};
430 use std::net::Ipv4Addr;
431
432 static IP_ADDR: IpAddr = IpAddr::V4(Ipv4Addr::new(192, 168, 0, 100));
433
434 #[test]
435 fn host_primary_ip_invariant() {
436 let mut host = Host::new(IP_ADDR);
437 let fresh_ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 5));
438 host.set_primary_ip(fresh_ip);
439
440 assert_eq!(host.primary_ip(), fresh_ip);
441 assert!(host.ips().contains(&fresh_ip));
442 }
443
444 #[test]
445 fn tarpit_boundary_test() {
446 let mut host = Host::new(IP_ADDR);
447 for i in 0..MAX_PORTS_PER_HOST {
448 host.add_port(Port::new(i as u16, Protocol::Tcp, PortState::Open));
449 }
450 assert!(!host.network_roles.contains(&NetworkRole::Tarpit));
451
452 host.add_port(Port::new(9999, Protocol::Tcp, PortState::Open));
453 assert!(host.network_roles.contains(&NetworkRole::Tarpit));
454 assert_eq!(host.port_count(), MAX_PORTS_PER_HOST);
455 }
456
457 #[test]
458 fn host_merge_promotes_status() {
459 let mut h1 = Host::new(IP_ADDR);
460 h1.set_status(HostStatus::Down);
461
462 let mut h2 = Host::new(IP_ADDR);
463 h2.set_status(HostStatus::Filtered);
464
465 h1.merge(h2);
466 assert_eq!(h1.status(), HostStatus::Filtered);
467 }
468
469 #[test]
470 fn merge_tarpit_collision_test() {
471 let mut h1 = Host::new(IP_ADDR);
472 for i in 0..600 {
473 h1.add_port(Port::new(i, Protocol::Tcp, PortState::Open));
474 }
475
476 let mut h2 = Host::new(IP_ADDR);
477 for i in 500..1100 {
480 h2.add_port(Port::new(i, Protocol::Tcp, PortState::Open));
481 }
482
483 h1.merge(h2);
484 assert_eq!(h1.port_count(), MAX_PORTS_PER_HOST);
485 assert!(h1.network_roles.contains(&NetworkRole::Tarpit));
486 }
487}