use std::collections::HashMap;
use std::time::Instant;
use crate::types::ApMap;
#[derive(Debug, Clone)]
pub enum AlertKind {
EvilTwin { essid: String, bssid1: String, bssid2: String },
DeauthFlood { bssid: String, essid: String },
NewAP { bssid: String, essid: String },
OpenNetwork { bssid: String, essid: String },
WepNetwork { bssid: String, essid: String },
ChannelOverlap { channel: u8, count: usize },
HandshakeCaptured { bssid: String, essid: String },
}
#[derive(Debug, Clone)]
pub struct Alert {
pub kind: AlertKind,
pub timestamp: Instant,
pub message: String,
pub dismissed: bool,
}
impl Alert {
pub fn new(kind: AlertKind) -> Self {
let message = match &kind {
AlertKind::EvilTwin { essid, bssid1, bssid2 } =>
format!("Evil twin: '{}' seen on {} and {}", essid, bssid1, bssid2),
AlertKind::DeauthFlood { essid, .. } =>
format!("Deauth flood detected targeting '{}'", essid),
AlertKind::NewAP { essid, bssid } =>
format!("New AP: '{}' ({})", essid, bssid),
AlertKind::OpenNetwork { essid, bssid } =>
format!("Open network: '{}' ({})", essid, bssid),
AlertKind::WepNetwork { essid, bssid } =>
format!("WEP network: '{}' ({}) - trivially crackable", essid, bssid),
AlertKind::ChannelOverlap { channel, count } =>
format!("Channel {} congested: {} APs", channel, count),
AlertKind::HandshakeCaptured { essid, bssid } =>
format!("Handshake captured: '{}' ({})", essid, bssid),
};
Self { kind, timestamp: Instant::now(), message, dismissed: false }
}
pub fn age_secs(&self) -> u64 {
self.timestamp.elapsed().as_secs()
}
}
pub struct AlertEngine {
pub alerts: Vec<Alert>,
known_bssids: HashMap<String, Instant>,
evil_twin_seen: HashMap<String, Vec<String>>, max_alerts: usize,
}
impl AlertEngine {
pub fn new() -> Self {
Self {
alerts: Vec::new(),
known_bssids: HashMap::new(),
evil_twin_seen: HashMap::new(),
max_alerts: 200,
}
}
pub fn active_alerts(&self) -> Vec<&Alert> {
self.alerts.iter()
.filter(|a| !a.dismissed && a.age_secs() < 300)
.collect()
}
pub fn dismiss_all(&mut self) {
for a in &mut self.alerts {
a.dismissed = true;
}
}
fn push(&mut self, alert: Alert) {
self.alerts.push(alert);
if self.alerts.len() > self.max_alerts {
self.alerts.remove(0);
}
}
pub fn scan(&mut self, ap_map: &ApMap) {
let map = ap_map.lock().unwrap();
for (bssid, ap) in map.iter() {
if !self.known_bssids.contains_key(bssid) {
self.known_bssids.insert(bssid.clone(), Instant::now());
if !ap.essid.is_empty() && ap.essid != "<hidden>" {
if self.known_bssids.len() > 5 {
self.push(Alert::new(AlertKind::NewAP {
bssid: bssid.clone(),
essid: ap.essid.clone(),
}));
}
if ap.encryption.display == "Open" {
self.push(Alert::new(AlertKind::OpenNetwork {
bssid: bssid.clone(),
essid: ap.essid.clone(),
}));
}
if ap.encryption.display.starts_with("WEP") {
self.push(Alert::new(AlertKind::WepNetwork {
bssid: bssid.clone(),
essid: ap.essid.clone(),
}));
}
}
}
}
self.evil_twin_seen.clear();
for (bssid, ap) in map.iter() {
if ap.essid.is_empty() || ap.essid == "<hidden>" { continue; }
self.evil_twin_seen
.entry(ap.essid.clone())
.or_default()
.push(bssid.clone());
}
let mut twin_alerts: Vec<Alert> = Vec::new();
for (essid, bssids) in &self.evil_twin_seen {
if bssids.len() < 2 { continue; }
let mut enc_set: HashMap<String, Vec<String>> = HashMap::new();
for bssid in bssids {
if let Some(ap) = map.get(bssid) {
enc_set.entry(ap.encryption.display.clone())
.or_default()
.push(bssid.clone());
}
}
if enc_set.len() > 1 {
let bssid_list: Vec<&String> = bssids.iter().take(2).collect();
let already_alerted = self.alerts.iter().any(|a| {
matches!(&a.kind, AlertKind::EvilTwin { essid: e, .. } if e == essid)
&& a.age_secs() < 300
});
if !already_alerted {
twin_alerts.push(Alert::new(AlertKind::EvilTwin {
essid: essid.clone(),
bssid1: bssid_list[0].clone(),
bssid2: bssid_list[1].clone(),
}));
}
}
}
for alert in twin_alerts {
self.push(alert);
}
let mut channel_counts: HashMap<u8, usize> = HashMap::new();
for ap in map.values() {
if ap.channel > 0 && ap.age_secs() < 60 {
*channel_counts.entry(ap.channel).or_default() += 1;
}
}
for (&ch, &count) in &channel_counts {
if count >= 15 {
let already = self.alerts.iter().any(|a| {
matches!(&a.kind, AlertKind::ChannelOverlap { channel, .. } if *channel == ch)
&& a.age_secs() < 120
});
if !already {
self.push(Alert::new(AlertKind::ChannelOverlap { channel: ch, count }));
}
}
}
}
pub fn handshake_captured(&mut self, bssid: &str, essid: &str) {
self.push(Alert::new(AlertKind::HandshakeCaptured {
bssid: bssid.to_string(),
essid: essid.to_string(),
}));
}
}
impl Default for AlertEngine {
fn default() -> Self {
Self::new()
}
}