Add deauth detection, Alerts tab, Search tab, node detail cards, UI improvements
This commit is contained in:
+210
-4
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user