sligh changed to dashbaord

This commit is contained in:
bot
2026-04-05 11:29:59 +03:00
parent 5c0ec8cc5a
commit 2636ea1e11
10 changed files with 364 additions and 42 deletions
+102 -21
View File
@@ -9,7 +9,7 @@ import sqlite3
import datetime
import os
import json
from fastapi import FastAPI, Request
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import HTMLResponse, StreamingResponse, FileResponse
DB_PATH = os.path.join(os.path.dirname(__file__), "events.db")
@@ -20,6 +20,46 @@ OUI_PATH = os.path.join(os.path.dirname(__file__), "oui.txt")
ONLINE_SECS = 30 # node considered online if heartbeat within this many seconds
EVENT_CAP = 100 # max events returned / shown in dashboard
# ─── Deauth reason codes ─────────────────────────────────────────────────────
# classification: 'normal', 'suspicious', 'attack'
DEAUTH_REASONS: dict[int, tuple[str, str]] = {
0: ("Reserved", "normal"),
1: ("Unspecified", "normal"),
2: ("Previous auth no longer valid", "attack"),
3: ("Station leaving BSS", "normal"),
4: ("Inactivity / idle timeout", "normal"),
5: ("Too many associated stations", "normal"),
6: ("Class 2 frame from non-authenticated station","attack"),
7: ("Class 3 frame from non-associated station", "attack"),
8: ("Station leaving BSS (disassoc)", "normal"),
9: ("Station not authenticated", "suspicious"),
10: ("Power capability unacceptable", "normal"),
11: ("Supported channels unacceptable", "normal"),
13: ("Invalid information element", "suspicious"),
14: ("MIC failure", "suspicious"),
15: ("4-way handshake timeout", "normal"),
16: ("Group key handshake timeout", "normal"),
17: ("IE in 4-way handshake differs", "suspicious"),
18: ("Invalid group cipher", "suspicious"),
19: ("Invalid pairwise cipher", "suspicious"),
20: ("Invalid AKMP", "suspicious"),
21: ("Unsupported RSNE version", "suspicious"),
22: ("Invalid RSNE capabilities", "suspicious"),
23: ("802.1X authentication failed", "suspicious"),
24: ("Cipher suite rejected by policy", "suspicious"),
34: ("Low ACK", "normal"),
39: ("BSS transition management request", "normal"),
45: ("Peer stakey negotiation failed", "suspicious"),
47: ("Authorized access limit reached", "normal"),
99: ("Peer abandoned session", "normal"),
103: ("Disassociated — excessive frames", "suspicious"),
147: ("Vendor-specific", "normal"),
}
def deauth_reason_info(code: int) -> tuple[str, str]:
return DEAUTH_REASONS.get(code, ("Unknown / vendor-specific", "normal"))
# ─── OUI lookup ──────────────────────────────────────────────────────────────
def _load_oui(path: str) -> dict[str, str]:
@@ -63,6 +103,7 @@ def oui_lookup(mac: str) -> tuple[str, bool]:
def get_conn() -> sqlite3.Connection:
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL")
return conn
@@ -147,6 +188,7 @@ def build_networks() -> list[dict]:
WHERE bssid IS NOT NULL
GROUP BY bssid
ORDER BY times_seen DESC
LIMIT 300
""")
return rows
@@ -169,13 +211,18 @@ def build_clients() -> list[dict]:
WHERE src_mac IS NOT NULL
GROUP BY src_mac
ORDER BY times_seen DESC
LIMIT 300
""")
# Fetch all probed SSIDs in one query, group in Python — avoids N+1
ssid_rows = query("""
# Fetch SSIDs only for the MACs we actually returned
if not rows:
return []
mac_list = [r["src_mac"] for r in rows]
placeholders = ",".join("?" * len(mac_list))
ssid_rows = query(f"""
SELECT DISTINCT src_mac, ssid FROM probe_events
WHERE src_mac IS NOT NULL AND ssid IS NOT NULL AND ssid != ''
WHERE src_mac IN ({placeholders}) AND ssid IS NOT NULL AND ssid != ''
ORDER BY src_mac, ssid
""")
""", tuple(mac_list))
ssids_by_mac: dict = {}
for s in ssid_rows:
ssids_by_mac.setdefault(s["src_mac"], []).append(s["ssid"])
@@ -199,18 +246,24 @@ def build_cross_node() -> dict:
rows = query("""
SELECT
bssid,
MAX(ssid) AS ssid,
MAX(channel) AS channel,
MAX(encryption) AS encryption,
node_id,
MAX(rssi) AS best_rssi,
ROUND(AVG(rssi), 1) AS avg_rssi,
COUNT(*) AS times_seen
FROM beacon_events
WHERE bssid IS NOT NULL
GROUP BY bssid, node_id
ORDER BY bssid, node_id
b.bssid,
MAX(b.ssid) AS ssid,
MAX(b.channel) AS channel,
MAX(b.encryption) AS encryption,
b.node_id,
MAX(b.rssi) AS best_rssi,
ROUND(AVG(b.rssi), 1) AS avg_rssi,
COUNT(*) AS times_seen
FROM beacon_events b
WHERE b.bssid IN (
SELECT bssid FROM beacon_events
WHERE bssid IS NOT NULL
GROUP BY bssid
ORDER BY COUNT(DISTINCT node_id) DESC, COUNT(*) DESC
LIMIT 300
)
GROUP BY b.bssid, b.node_id
ORDER BY b.bssid, b.node_id
""")
networks: dict = {}
@@ -455,12 +508,41 @@ def build_alerts() -> dict:
d["vendor"] = vendor
d["randomized"] = randomized
# Reason code breakdown — all time
reason_rows = query("""
SELECT reason,
COUNT(*) AS total_frames,
COUNT(DISTINCT bssid) AS unique_bssids,
COUNT(DISTINCT dst) AS unique_targets,
MIN(received_at) AS first_seen,
MAX(received_at) AS last_seen
FROM deauth_events
GROUP BY reason
ORDER BY total_frames DESC
""")
for r in reason_rows:
desc, classification = deauth_reason_info(r["reason"])
r["description"] = desc
r["classification"] = classification
# Activity heatmap — frame count per day × hour
heatmap_rows = query("""
SELECT date(received_at) AS day,
CAST(strftime('%H', received_at) AS INTEGER) AS hour,
COUNT(*) AS frames
FROM deauth_events
GROUP BY day, hour
ORDER BY day, hour
""")
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,
"reason_stats": reason_rows,
"heatmap": heatmap_rows,
}
@@ -792,7 +874,6 @@ async def api_heartbeats():
async def api_node_detail(node_id: str):
detail = build_node_detail(node_id)
if detail is None:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="Node not found")
return detail