sligh changed to dashbaord
This commit is contained in:
+102
-21
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user