Add deauth/disassoc detection — firmware capture, coordinator ingestion, Alerts tab with burst detection
This commit is contained in:
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user