mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
Count distinct devices (by MAC) in active scanner status
The ARP and SNMP scanners reported every online sighting, so a device seen on multiple IPs or via duplicate ARP replies was counted more than once. Fold the dedup into ActiveStatus::record_scan, which now takes the device slice and reports the number of distinct MAC addresses. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
844d7d189c
commit
6b044a6739
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
## Backend
|
## Backend
|
||||||
|
|
||||||
- [ ] In the active scanners status, it should count the number of distinct devices it saw on the last scan (avoid duplicates)
|
- [x] In the active scanners status, it should count the number of distinct devices it saw on the last scan (avoid duplicates)
|
||||||
- [ ] Implement the pushover API call directly to support HTML content and review notification text to use it
|
- [ ] Implement the pushover API call directly to support HTML content and review notification text to use it
|
||||||
|
|
||||||
## Frontend
|
## Frontend
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ pub async fn scan() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
info!("Done with ARP probes");
|
info!("Done with ARP probes");
|
||||||
info!("Found {} online devices", devices.len());
|
info!("Found {} online devices", devices.len());
|
||||||
status::record_scan(devices.len() as u64);
|
status::record_scan(&devices);
|
||||||
|
|
||||||
// Process found devices
|
// Process found devices
|
||||||
for device in devices.iter() {
|
for device in devices.iter() {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use chrono::{DateTime, Utc};
|
|||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::model::devices::Device;
|
||||||
use crate::scanners::common::active_status::{ActiveSnapshot, ActiveStatus};
|
use crate::scanners::common::active_status::{ActiveSnapshot, ActiveStatus};
|
||||||
|
|
||||||
static STATUS: OnceCell<Mutex<ActiveStatus>> = OnceCell::new();
|
static STATUS: OnceCell<Mutex<ActiveStatus>> = OnceCell::new();
|
||||||
@@ -22,9 +23,9 @@ pub fn set_waiting(next_scan_at: DateTime<Utc>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn record_scan(devices_seen: u64) {
|
pub fn record_scan(devices: &[Device]) {
|
||||||
if let Some(m) = STATUS.get() {
|
if let Some(m) = STATUS.get() {
|
||||||
m.lock().unwrap().record_scan(devices_seen);
|
m.lock().unwrap().record_scan(devices);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
use crate::model::devices::Device;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
/// Status state for the active (polling) scanners — ARP and SNMP. Each scanner owns its own
|
/// Status state for the active (polling) scanners — ARP and SNMP. Each scanner owns its own
|
||||||
/// `OnceCell<Mutex<ActiveStatus>>` and delegates to these methods (see e.g.
|
/// `OnceCell<Mutex<ActiveStatus>>` and delegates to these methods (see e.g.
|
||||||
@@ -41,9 +43,16 @@ impl ActiveStatus {
|
|||||||
self.next_scan_at = Some(next_scan_at);
|
self.next_scan_at = Some(next_scan_at);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Record the result of a completed scan.
|
/// Record the result of a completed scan. A single scan can surface the same device more than
|
||||||
pub fn record_scan(&mut self, devices_seen: u64) {
|
/// once — duplicate ARP replies, or one MAC bound to several IPs in an SNMP ARP cache — so the
|
||||||
self.last_scan_devices_seen = Some(devices_seen);
|
/// reported count is the number of distinct devices (by MAC address), not raw sightings.
|
||||||
|
pub fn record_scan(&mut self, devices: &[Device]) {
|
||||||
|
let distinct = devices
|
||||||
|
.iter()
|
||||||
|
.map(|device| &device.mac_address)
|
||||||
|
.collect::<HashSet<_>>()
|
||||||
|
.len();
|
||||||
|
self.last_scan_devices_seen = Some(distinct as u64);
|
||||||
self.last_scan_at = Some(Utc::now());
|
self.last_scan_at = Some(Utc::now());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,15 +92,37 @@ mod tests {
|
|||||||
assert_eq!(snapshot.next_scan_at.unwrap(), next);
|
assert_eq!(snapshot.next_scan_at.unwrap(), next);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn device_with_mac(mac: &str, ip: &str) -> Device {
|
||||||
|
Device::new(mac.to_string(), ip.to_string(), String::new(), Utc::now())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn record_scan_stores_count_and_time() {
|
fn record_scan_stores_count_and_time() {
|
||||||
let mut status = ActiveStatus::new();
|
let mut status = ActiveStatus::new();
|
||||||
status.record_scan(7);
|
let devices = vec![
|
||||||
|
device_with_mac("aa:bb:cc:dd:ee:01", "192.168.0.2"),
|
||||||
|
device_with_mac("aa:bb:cc:dd:ee:02", "192.168.0.3"),
|
||||||
|
];
|
||||||
|
status.record_scan(&devices);
|
||||||
let snapshot = status.snapshot();
|
let snapshot = status.snapshot();
|
||||||
assert_eq!(snapshot.last_scan_devices_seen, Some(7));
|
assert_eq!(snapshot.last_scan_devices_seen, Some(2));
|
||||||
assert!(snapshot.last_scan_at.is_some());
|
assert!(snapshot.last_scan_at.is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn record_scan_counts_distinct_devices() {
|
||||||
|
let mut status = ActiveStatus::new();
|
||||||
|
// Same MAC seen twice (e.g. duplicate ARP reply or one MAC on two IPs) plus a second
|
||||||
|
// distinct device — only two unique devices should be reported.
|
||||||
|
let devices = vec![
|
||||||
|
device_with_mac("aa:bb:cc:dd:ee:01", "192.168.0.2"),
|
||||||
|
device_with_mac("aa:bb:cc:dd:ee:01", "192.168.0.9"),
|
||||||
|
device_with_mac("aa:bb:cc:dd:ee:02", "192.168.0.3"),
|
||||||
|
];
|
||||||
|
status.record_scan(&devices);
|
||||||
|
assert_eq!(status.snapshot().last_scan_devices_seen, Some(2));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn initial_state_is_empty() {
|
fn initial_state_is_empty() {
|
||||||
let snapshot = ActiveStatus::new().snapshot();
|
let snapshot = ActiveStatus::new().snapshot();
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ pub async fn scan() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
match finder::find(config).await {
|
match finder::find(config).await {
|
||||||
Ok(devices) => {
|
Ok(devices) => {
|
||||||
info!("SNMP poll found {} devices in the ARP cache", devices.len());
|
info!("SNMP poll found {} devices in the ARP cache", devices.len());
|
||||||
status::record_scan(devices.len() as u64);
|
status::record_scan(&devices);
|
||||||
for device in devices.iter() {
|
for device in devices.iter() {
|
||||||
pipeline::record_sighting(device.clone(), DeviceEventScanner::Snmp);
|
pipeline::record_sighting(device.clone(), DeviceEventScanner::Snmp);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use chrono::{DateTime, Utc};
|
|||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::model::devices::Device;
|
||||||
use crate::scanners::common::active_status::{ActiveSnapshot, ActiveStatus};
|
use crate::scanners::common::active_status::{ActiveSnapshot, ActiveStatus};
|
||||||
|
|
||||||
static STATUS: OnceCell<Mutex<ActiveStatus>> = OnceCell::new();
|
static STATUS: OnceCell<Mutex<ActiveStatus>> = OnceCell::new();
|
||||||
@@ -22,9 +23,9 @@ pub fn set_waiting(next_scan_at: DateTime<Utc>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn record_scan(devices_seen: u64) {
|
pub fn record_scan(devices: &[Device]) {
|
||||||
if let Some(m) = STATUS.get() {
|
if let Some(m) = STATUS.get() {
|
||||||
m.lock().unwrap().record_scan(devices_seen);
|
m.lock().unwrap().record_scan(devices);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user