Add deauth/disassoc detection — firmware capture, coordinator ingestion, Alerts tab with burst detection

This commit is contained in:
bot
2026-04-03 22:24:25 +03:00
parent cbef4143ee
commit fbf5b5c738
7 changed files with 426 additions and 23 deletions
+82
View File
@@ -354,6 +354,69 @@ def build_presence() -> dict:
}
def build_alerts() -> dict:
now = datetime.datetime.utcnow()
cutoff_5m = (now - datetime.timedelta(minutes=5)).isoformat(timespec="seconds")
cutoff_1h = (now - datetime.timedelta(hours=1)).isoformat(timespec="seconds")
# Recent raw deauth/disassoc events
recent = query("""
SELECT * FROM deauth_events
ORDER BY id DESC LIMIT 100
""")
# Burst detection: >= 10 frames for the same BSSID within the last 5 minutes.
# node_count > 1 means multiple sensors confirmed the burst — much higher confidence.
bursts = query("""
SELECT
bssid,
subtype,
COUNT(*) AS count,
COUNT(DISTINCT node_id) AS node_count,
COUNT(DISTINCT src) AS unique_srcs,
MIN(received_at) AS first_seen,
MAX(received_at) AS last_seen
FROM deauth_events
WHERE received_at > ?
GROUP BY bssid, subtype
HAVING COUNT(*) >= 10
ORDER BY count DESC
""", (cutoff_5m,))
# Correlate burst BSSIDs with known network SSIDs
if bursts:
bssid_list = [b["bssid"] for b in bursts]
placeholders = ",".join("?" * len(bssid_list))
ssid_rows = query(f"""
SELECT bssid, MAX(ssid) AS ssid, MAX(encryption) AS encryption
FROM beacon_events WHERE bssid IN ({placeholders})
GROUP BY bssid
""", tuple(bssid_list))
ssid_map = {r["bssid"]: r for r in ssid_rows}
for b in bursts:
net = ssid_map.get(b["bssid"], {})
b["ssid"] = net.get("ssid")
b["encryption"] = net.get("encryption")
# Summary counts for the last hour
summary = query("""
SELECT
COUNT(*) AS total,
COUNT(DISTINCT bssid) AS unique_bssids,
COUNT(DISTINCT src) AS unique_srcs,
SUM(CASE WHEN subtype='deauth' THEN 1 ELSE 0 END) AS deauth_count,
SUM(CASE WHEN subtype='disassoc' THEN 1 ELSE 0 END) AS disassoc_count
FROM deauth_events
WHERE received_at > ?
""", (cutoff_1h,))
return {
"recent": recent,
"bursts": bursts,
"summary": summary[0] if summary else {},
}
def build_node_detail(node_id: str) -> dict | None:
rows = query("""
SELECT
@@ -418,6 +481,20 @@ def ensure_schema():
uptime_ms INTEGER, free_heap INTEGER, wifi_rssi INTEGER
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS deauth_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
received_at TEXT NOT NULL,
node_id TEXT,
node_ts INTEGER,
subtype TEXT,
src TEXT,
dst TEXT,
bssid TEXT,
reason INTEGER,
rssi INTEGER
)
""")
conn.commit()
@@ -490,6 +567,11 @@ async def api_rssi_history(bssid: str, hours: int = 2):
return series
@app.get("/api/alerts")
async def api_alerts():
return build_alerts()
@app.get("/api/heartbeats")
async def api_heartbeats():
"""Latest heartbeat per node."""