Add deauth detection, Alerts tab, Search tab, node detail cards, UI improvements

This commit is contained in:
bot
2026-04-04 12:47:43 +03:00
parent fbf5b5c738
commit 5c0ec8cc5a
7 changed files with 668 additions and 43 deletions
+210 -4
View File
@@ -93,7 +93,7 @@ def build_nodes() -> list[dict]:
MIN(received_at) AS first_seen
FROM beacon_events
GROUP BY node_id
ORDER BY last_beacon DESC
ORDER BY node_id ASC
""")
# Latest heartbeat per node
hb_rows = query("""
@@ -410,13 +410,214 @@ def build_alerts() -> dict:
WHERE received_at > ?
""", (cutoff_1h,))
# Most targeted BSSIDs — all time
top_targets = query("""
SELECT
bssid,
COUNT(*) AS total_frames,
COUNT(DISTINCT src) AS unique_srcs,
COUNT(DISTINCT node_id) AS node_count,
MIN(received_at) AS first_seen,
MAX(received_at) AS last_seen
FROM deauth_events
GROUP BY bssid
ORDER BY total_frames DESC
LIMIT 10
""")
if top_targets:
bssid_list = [t["bssid"] for t in top_targets]
placeholders = ",".join("?" * len(bssid_list))
ssid_rows = query(f"""
SELECT bssid, MAX(ssid) AS ssid
FROM beacon_events WHERE bssid IN ({placeholders})
GROUP BY bssid
""", tuple(bssid_list))
ssid_map = {r["bssid"]: r["ssid"] for r in ssid_rows}
for t in top_targets:
t["ssid"] = ssid_map.get(t["bssid"])
# Most targeted devices — all time (grouped by dst, the actual victim)
top_targeted_devices = query("""
SELECT
dst,
COUNT(*) AS total_frames,
COUNT(DISTINCT bssid) AS networks_used,
COUNT(DISTINCT node_id) AS node_count,
MIN(received_at) AS first_seen,
MAX(received_at) AS last_seen
FROM deauth_events
GROUP BY dst
ORDER BY total_frames DESC
LIMIT 10
""")
for d in top_targeted_devices:
vendor, randomized = oui_lookup(d["dst"])
d["vendor"] = vendor
d["randomized"] = randomized
return {
"recent": recent,
"bursts": bursts,
"summary": summary[0] if summary else {},
"recent": recent,
"bursts": bursts,
"summary": summary[0] if summary else {},
"top_targets": top_targets,
"top_targeted_devices": top_targeted_devices,
}
def build_search(q: str) -> dict:
if not q or len(q) < 2:
return {}
like = f"%{q}%"
results = {}
# As a network — BSSID or SSID match in beacon_events
as_network = query("""
SELECT bssid, MAX(ssid) AS ssid,
COUNT(*) AS times_seen,
MAX(rssi) AS best_rssi, ROUND(AVG(rssi),1) AS avg_rssi,
MAX(channel) AS channel, MAX(encryption) AS encryption,
COUNT(DISTINCT node_id) AS node_count,
MIN(received_at) AS first_seen, MAX(received_at) AS last_seen
FROM beacon_events
WHERE bssid LIKE ? OR ssid LIKE ?
GROUP BY bssid ORDER BY times_seen DESC LIMIT 20
""", (like, like))
if as_network:
results["as_network"] = as_network
# As a client device — src_mac match in probe_events
as_client = query("""
SELECT src_mac,
COUNT(*) AS times_seen,
MAX(rssi) AS best_rssi, ROUND(AVG(rssi),1) AS avg_rssi,
COUNT(DISTINCT node_id) AS node_count,
MIN(received_at) AS first_seen, MAX(received_at) AS last_seen
FROM probe_events
WHERE src_mac LIKE ?
GROUP BY src_mac LIMIT 10
""", (like,))
if as_client:
ssid_rows = query("""
SELECT DISTINCT src_mac, ssid FROM probe_events
WHERE src_mac LIKE ? AND ssid IS NOT NULL AND ssid != ''
""", (like,))
ssids_by_mac: dict = {}
for s in ssid_rows:
ssids_by_mac.setdefault(s["src_mac"], []).append(s["ssid"])
for r in as_client:
vendor, randomized = oui_lookup(r["src_mac"])
r["vendor"] = vendor
r["randomized"] = randomized
r["probed_ssids"] = ssids_by_mac.get(r["src_mac"], [])
results["as_client"] = as_client
# Devices probing for this SSID
probing_for = query("""
SELECT src_mac,
COUNT(*) AS times_seen,
MAX(rssi) AS best_rssi,
MIN(received_at) AS first_seen, MAX(received_at) AS last_seen
FROM probe_events
WHERE ssid LIKE ?
GROUP BY src_mac ORDER BY times_seen DESC LIMIT 20
""", (like,))
if probing_for:
for r in probing_for:
vendor, randomized = oui_lookup(r["src_mac"])
r["vendor"] = vendor
r["randomized"] = randomized
results["probing_for"] = probing_for
# As a deauth victim — dst match
as_victim = query("""
SELECT dst,
COUNT(*) AS total_frames,
COUNT(DISTINCT bssid) AS networks_used,
COUNT(DISTINCT node_id) AS node_count,
MIN(received_at) AS first_seen, MAX(received_at) AS last_seen
FROM deauth_events
WHERE dst LIKE ?
GROUP BY dst LIMIT 10
""", (like,))
if as_victim:
for r in as_victim:
vendor, randomized = oui_lookup(r["dst"])
r["vendor"] = vendor
r["randomized"] = randomized
results["as_victim"] = as_victim
# As an impersonated AP — bssid match in deauth_events
as_impersonated = query("""
SELECT bssid,
COUNT(*) AS total_frames,
COUNT(DISTINCT dst) AS unique_targets,
COUNT(DISTINCT node_id) AS node_count,
MIN(received_at) AS first_seen, MAX(received_at) AS last_seen
FROM deauth_events
WHERE bssid LIKE ?
GROUP BY bssid LIMIT 10
""", (like,))
if as_impersonated:
results["as_impersonated"] = as_impersonated
# Vendor name search — match query against OUI database, then find those MACs
q_lower = q.lower()
matching_prefixes = [
prefix for prefix, vendor in _OUI_DB.items()
if q_lower in vendor.lower()
][:60] # cap at 60 prefixes to keep query manageable
if matching_prefixes:
ph = ",".join("?" * len(matching_prefixes))
vendor_clients = query(f"""
SELECT src_mac,
COUNT(*) AS times_seen,
MAX(rssi) AS best_rssi, ROUND(AVG(rssi),1) AS avg_rssi,
COUNT(DISTINCT node_id) AS node_count,
MIN(received_at) AS first_seen, MAX(received_at) AS last_seen
FROM probe_events
WHERE substr(src_mac,1,8) IN ({ph})
GROUP BY src_mac ORDER BY times_seen DESC LIMIT 30
""", tuple(matching_prefixes))
if vendor_clients:
ssid_rows = query(f"""
SELECT DISTINCT src_mac, ssid FROM probe_events
WHERE substr(src_mac,1,8) IN ({ph})
AND ssid IS NOT NULL AND ssid != ''
""", tuple(matching_prefixes))
ssids_by_mac: dict = {}
for s in ssid_rows:
ssids_by_mac.setdefault(s["src_mac"], []).append(s["ssid"])
for r in vendor_clients:
vendor, randomized = oui_lookup(r["src_mac"])
r["vendor"] = vendor
r["randomized"] = randomized
r["probed_ssids"] = ssids_by_mac.get(r["src_mac"], [])
results["vendor_clients"] = vendor_clients
vendor_networks = query(f"""
SELECT bssid, MAX(ssid) AS ssid,
COUNT(*) AS times_seen,
MAX(rssi) AS best_rssi, ROUND(AVG(rssi),1) AS avg_rssi,
MAX(channel) AS channel, MAX(encryption) AS encryption,
COUNT(DISTINCT node_id) AS node_count,
MIN(received_at) AS first_seen, MAX(received_at) AS last_seen
FROM beacon_events
WHERE substr(bssid,1,8) IN ({ph})
GROUP BY bssid ORDER BY times_seen DESC LIMIT 30
""", tuple(matching_prefixes))
if vendor_networks:
results["vendor_networks"] = vendor_networks
if "vendor_clients" in results or "vendor_networks" in results:
results["vendor_name"] = next(
(v for k, v in _OUI_DB.items() if q_lower in v.lower()), q
)
return results
def build_node_detail(node_id: str) -> dict | None:
rows = query("""
SELECT
@@ -567,6 +768,11 @@ async def api_rssi_history(bssid: str, hours: int = 2):
return series
@app.get("/api/search")
async def api_search(q: str = ""):
return build_search(q)
@app.get("/api/alerts")
async def api_alerts():
return build_alerts()