856 lines
11 KiB
Python
856 lines
11 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
|
||
"""
|
||
Dashboard HTML report.
|
||
|
||
Главная страница отчета.
|
||
|
||
Содержит только обзорную информацию.
|
||
|
||
Без карты.
|
||
|
||
Без полного списка точек доступа.
|
||
|
||
Вся детальная информация находится
|
||
на страницах отдельных поездок.
|
||
"""
|
||
|
||
from pathlib import Path
|
||
|
||
from .config import (
|
||
RECENT_SESSIONS_LIMIT,
|
||
)
|
||
|
||
from .utils import (
|
||
format_datetime,
|
||
)
|
||
|
||
from .template import (
|
||
|
||
page_begin,
|
||
|
||
page_end,
|
||
|
||
page_header,
|
||
|
||
card,
|
||
|
||
stat,
|
||
|
||
stats_grid,
|
||
|
||
)
|
||
|
||
from .statistics import (
|
||
|
||
database_statistics,
|
||
|
||
top_vendors,
|
||
|
||
top_essid,
|
||
|
||
channel_statistics,
|
||
|
||
encryption_statistics,
|
||
|
||
)
|
||
|
||
from .sessions import (
|
||
|
||
recent_sessions,
|
||
|
||
all_sessions,
|
||
|
||
)
|
||
|
||
# ============================================================
|
||
# Helpers
|
||
# ============================================================
|
||
|
||
def format_session_folder(capture_file):
|
||
if not capture_file:
|
||
return "-"
|
||
|
||
folder = Path(capture_file).parent.name
|
||
|
||
try:
|
||
from datetime import datetime, timezone
|
||
|
||
dt = datetime.strptime(
|
||
folder,
|
||
"%Y%m%d_%H%M%S"
|
||
).replace(
|
||
tzinfo=timezone.utc
|
||
)
|
||
|
||
return format_datetime(
|
||
dt.isoformat()
|
||
)
|
||
|
||
except ValueError:
|
||
return folder
|
||
|
||
|
||
# ============================================================
|
||
# Database statistics
|
||
# ============================================================
|
||
|
||
def statistics_block(conn):
|
||
|
||
data = database_statistics(conn)
|
||
|
||
items = [
|
||
|
||
stat(
|
||
data["access_points"],
|
||
"Access Points"
|
||
),
|
||
|
||
stat(
|
||
data["access_points_without_coordinates"],
|
||
"AP without Coordinates"
|
||
),
|
||
|
||
stat(
|
||
data["sessions"],
|
||
"Capture Sessions"
|
||
),
|
||
|
||
stat(
|
||
data["vendors"],
|
||
"Vendors"
|
||
),
|
||
|
||
stat(
|
||
data["handshakes"],
|
||
"Handshakes"
|
||
),
|
||
|
||
stat(
|
||
data["credentials"],
|
||
"Handshake Cracked"
|
||
),
|
||
|
||
stat(
|
||
data["pmkid"],
|
||
"PMKID"
|
||
),
|
||
|
||
stat(
|
||
data["cracked"],
|
||
"Cracked AP"
|
||
),
|
||
|
||
stat(
|
||
data["database_size"],
|
||
"Database"
|
||
),
|
||
|
||
]
|
||
|
||
return card(
|
||
|
||
"📊 Database Overview",
|
||
|
||
stats_grid(items)
|
||
|
||
)
|
||
|
||
|
||
# ============================================================
|
||
# Information
|
||
# ============================================================
|
||
|
||
def information_block(conn):
|
||
|
||
data = database_statistics(conn)
|
||
|
||
html = f"""
|
||
|
||
<table>
|
||
|
||
<tr>
|
||
|
||
<td><b>Database size</b></td>
|
||
|
||
<td>{data["database_size"]}</td>
|
||
|
||
</tr>
|
||
|
||
<tr>
|
||
|
||
<td><b>Captures size</b></td>
|
||
|
||
<td>{data["captures_size"]}</td>
|
||
|
||
</tr>
|
||
|
||
<tr>
|
||
|
||
<td><b>Capture files</b></td>
|
||
|
||
<td>{data["capture_files"]}</td>
|
||
|
||
</tr>
|
||
|
||
<tr>
|
||
|
||
<td><b>First access point</b></td>
|
||
|
||
<td>{format_datetime(data["first_seen"])}</td>
|
||
|
||
</tr>
|
||
|
||
<tr>
|
||
|
||
<td><b>Last access point</b></td>
|
||
|
||
<td>{format_datetime(data["last_seen"])}</td>
|
||
|
||
</tr>
|
||
|
||
</table>
|
||
|
||
"""
|
||
|
||
return card(
|
||
|
||
"ℹ️ Database Information",
|
||
|
||
html,
|
||
|
||
)
|
||
# ============================================================
|
||
# Top Vendors
|
||
# ============================================================
|
||
|
||
def vendors_block(conn):
|
||
|
||
rows = top_vendors(
|
||
conn,
|
||
limit=15
|
||
)
|
||
|
||
html = """
|
||
|
||
<table>
|
||
|
||
<tr>
|
||
|
||
<th>Vendor</th>
|
||
|
||
<th>Devices</th>
|
||
|
||
</tr>
|
||
|
||
"""
|
||
|
||
for vendor, total in rows:
|
||
|
||
if not vendor:
|
||
|
||
vendor = "-"
|
||
|
||
html += f"""
|
||
|
||
<tr>
|
||
|
||
<td>{vendor}</td>
|
||
|
||
<td>{total}</td>
|
||
|
||
</tr>
|
||
|
||
"""
|
||
|
||
html += """
|
||
|
||
</table>
|
||
|
||
"""
|
||
|
||
return card(
|
||
|
||
"🏭 Top Vendors",
|
||
|
||
html,
|
||
|
||
)
|
||
|
||
|
||
# ============================================================
|
||
# Top ESSID
|
||
# ============================================================
|
||
|
||
def essid_block(conn):
|
||
|
||
rows = top_essid(
|
||
conn,
|
||
limit=20
|
||
)
|
||
|
||
html = """
|
||
|
||
<table>
|
||
|
||
<tr>
|
||
|
||
<th>ESSID</th>
|
||
|
||
<th>Count</th>
|
||
|
||
</tr>
|
||
|
||
"""
|
||
|
||
for essid, total in rows:
|
||
|
||
if not essid:
|
||
|
||
essid = "<hidden>"
|
||
|
||
html += f"""
|
||
|
||
<tr>
|
||
|
||
<td>{essid}</td>
|
||
|
||
<td>{total}</td>
|
||
|
||
</tr>
|
||
|
||
"""
|
||
|
||
html += """
|
||
|
||
</table>
|
||
|
||
"""
|
||
|
||
return card(
|
||
|
||
"📶 Most Common ESSID",
|
||
|
||
html,
|
||
|
||
)
|
||
|
||
|
||
# ============================================================
|
||
# Channels
|
||
# ============================================================
|
||
|
||
def channels_block(conn):
|
||
|
||
rows = channel_statistics(
|
||
conn
|
||
)
|
||
|
||
html = """
|
||
|
||
<table>
|
||
|
||
<tr>
|
||
|
||
<th>Channel</th>
|
||
|
||
<th>Access Points</th>
|
||
|
||
</tr>
|
||
|
||
"""
|
||
|
||
for channel, total in rows:
|
||
|
||
html += f"""
|
||
|
||
<tr>
|
||
|
||
<td>{channel}</td>
|
||
|
||
<td>{total}</td>
|
||
|
||
</tr>
|
||
|
||
"""
|
||
|
||
html += """
|
||
|
||
</table>
|
||
|
||
"""
|
||
|
||
return card(
|
||
|
||
"📡 Channel Statistics",
|
||
|
||
html,
|
||
|
||
)
|
||
|
||
|
||
# ============================================================
|
||
# Encryption
|
||
# ============================================================
|
||
|
||
def encryption_block(conn):
|
||
|
||
rows = encryption_statistics(
|
||
conn
|
||
)
|
||
|
||
html = """
|
||
|
||
<table>
|
||
|
||
<tr>
|
||
|
||
<th>Encryption</th>
|
||
|
||
<th>Networks</th>
|
||
|
||
</tr>
|
||
|
||
"""
|
||
|
||
for encryption, total in rows:
|
||
|
||
if not encryption:
|
||
|
||
encryption = "-"
|
||
|
||
html += f"""
|
||
|
||
<tr>
|
||
|
||
<td>{encryption}</td>
|
||
|
||
<td>{total}</td>
|
||
|
||
</tr>
|
||
|
||
"""
|
||
|
||
html += """
|
||
|
||
</table>
|
||
|
||
"""
|
||
|
||
return card(
|
||
|
||
"🔐 Encryption",
|
||
|
||
html,
|
||
|
||
)
|
||
# ============================================================
|
||
# Session Card
|
||
# ============================================================
|
||
|
||
def session_card(
|
||
session,
|
||
context=None,
|
||
):
|
||
|
||
session_url = context.session_url(
|
||
session["id"]
|
||
)
|
||
|
||
return f"""
|
||
|
||
<div class="card">
|
||
|
||
<h3>
|
||
|
||
🚗 Session {format_datetime(session.get("trip_start"))}
|
||
|
||
</h3>
|
||
|
||
<table>
|
||
|
||
<tr>
|
||
|
||
<td><b>Trip start</b></td>
|
||
|
||
<td>{format_datetime(session.get("trip_start"))}</td>
|
||
|
||
</tr>
|
||
|
||
<tr>
|
||
|
||
<td><b>Trip end</b></td>
|
||
|
||
<td>{format_datetime(session.get("trip_end"))}</td>
|
||
|
||
</tr>
|
||
|
||
<tr>
|
||
|
||
<td><b>Duration</b></td>
|
||
|
||
<td>{session.get("duration","-")}</td>
|
||
|
||
</tr>
|
||
|
||
<tr>
|
||
|
||
<td><b>Imported</b></td>
|
||
|
||
<td>{format_datetime(session.get("start_time"))}</td>
|
||
|
||
</tr>
|
||
|
||
<tr>
|
||
|
||
<td><b>Session folder</b></td>
|
||
|
||
<td>{format_session_folder(session.get("capture_file"))}</td>
|
||
|
||
</tr>
|
||
|
||
<tr>
|
||
|
||
<td><b>Access Points</b></td>
|
||
|
||
<td>{session.get("access_points","-")}</td>
|
||
|
||
</tr>
|
||
|
||
<tr>
|
||
|
||
<td><b>Coordinates Restored</b></td>
|
||
|
||
<td>{session.get("coordinates_restored","-")}</td>
|
||
|
||
</tr>
|
||
|
||
<tr>
|
||
|
||
<td><b>AP without Coordinates</b></td>
|
||
|
||
<td>{session.get("ap_without_coordinates","-")}</td>
|
||
|
||
</tr>
|
||
|
||
<tr>
|
||
|
||
<td><b>Handshake AP</b></td>
|
||
|
||
<td>{session.get("handshakes","-")}</td>
|
||
|
||
</tr>
|
||
|
||
<tr>
|
||
|
||
<td><b>Handshake Records</b></td>
|
||
|
||
<td>{session.get("handshake_records","-")}</td>
|
||
|
||
</tr>
|
||
|
||
<tr>
|
||
|
||
<td><b>PMKID AP</b></td>
|
||
|
||
<td>{session.get("pmkid","-")}</td>
|
||
|
||
</tr>
|
||
|
||
<tr>
|
||
|
||
<td><b>Cracked AP</b></td>
|
||
|
||
<td>{session.get("cracked","-")}</td>
|
||
|
||
</tr>
|
||
|
||
<tr>
|
||
|
||
<td><b>Directory size</b></td>
|
||
|
||
<td>{session.get("directory_size","-")}</td>
|
||
|
||
</tr>
|
||
|
||
</table>
|
||
|
||
<p>
|
||
|
||
<a
|
||
|
||
class="button"
|
||
draggable="false"
|
||
|
||
href="{session_url}">
|
||
|
||
📄 Open Session Report
|
||
|
||
</a>
|
||
|
||
</p>
|
||
|
||
</div>
|
||
|
||
"""
|
||
|
||
|
||
# ============================================================
|
||
# Recent Sessions
|
||
# ============================================================
|
||
|
||
def sessions_block(
|
||
conn,
|
||
context=None,
|
||
):
|
||
|
||
sessions = recent_sessions(
|
||
|
||
conn,
|
||
|
||
limit=RECENT_SESSIONS_LIMIT,
|
||
|
||
)
|
||
|
||
if not sessions:
|
||
|
||
return card(
|
||
|
||
"🚗 Recent Capture Sessions",
|
||
|
||
"""
|
||
|
||
<p>
|
||
|
||
Capture sessions not found.
|
||
|
||
</p>
|
||
|
||
"""
|
||
|
||
)
|
||
|
||
cards = []
|
||
|
||
for session in sessions:
|
||
|
||
cards.append(
|
||
|
||
session_card(
|
||
|
||
session,
|
||
context,
|
||
|
||
)
|
||
|
||
)
|
||
|
||
capture_sessions_url = (
|
||
context.capture_sessions_url()
|
||
)
|
||
|
||
html = """
|
||
|
||
<div class="report-grid">
|
||
|
||
"""
|
||
|
||
html += "\n".join(cards)
|
||
|
||
html += f"""
|
||
|
||
</div>
|
||
|
||
<p class="report-action">
|
||
|
||
<a
|
||
class="button"
|
||
draggable="false"
|
||
href="{capture_sessions_url}">
|
||
|
||
Show all sessions
|
||
|
||
</a>
|
||
|
||
</p>
|
||
|
||
"""
|
||
|
||
return card(
|
||
|
||
"🚗 Recent Capture Sessions",
|
||
|
||
html,
|
||
|
||
)
|
||
# ============================================================
|
||
# Dashboard
|
||
# ============================================================
|
||
|
||
def generate_dashboard(
|
||
conn,
|
||
context=None,
|
||
):
|
||
|
||
html = page_begin(
|
||
|
||
title="WiFi GPS Mapper",
|
||
|
||
description=(
|
||
"Database overview"
|
||
),
|
||
|
||
active="dashboard",
|
||
|
||
context=context,
|
||
|
||
)
|
||
|
||
# --------------------------------------------------------
|
||
# Database statistics
|
||
# --------------------------------------------------------
|
||
|
||
html += statistics_block(
|
||
conn
|
||
)
|
||
|
||
# --------------------------------------------------------
|
||
# Information
|
||
# --------------------------------------------------------
|
||
|
||
html += information_block(
|
||
conn
|
||
)
|
||
|
||
# --------------------------------------------------------
|
||
# Vendors / ESSID
|
||
# --------------------------------------------------------
|
||
|
||
html += """
|
||
|
||
<div class="report-grid">
|
||
|
||
"""
|
||
|
||
html += vendors_block(
|
||
conn
|
||
)
|
||
|
||
html += essid_block(
|
||
conn
|
||
)
|
||
|
||
html += """
|
||
|
||
</div>
|
||
|
||
"""
|
||
|
||
# --------------------------------------------------------
|
||
# Channels / Encryption
|
||
# --------------------------------------------------------
|
||
|
||
html += """
|
||
|
||
<div class="report-grid">
|
||
|
||
"""
|
||
|
||
html += channels_block(
|
||
conn
|
||
)
|
||
|
||
html += encryption_block(
|
||
conn
|
||
)
|
||
|
||
html += """
|
||
|
||
</div>
|
||
|
||
"""
|
||
|
||
# --------------------------------------------------------
|
||
# Sessions
|
||
# --------------------------------------------------------
|
||
|
||
html += sessions_block(
|
||
conn,
|
||
context,
|
||
)
|
||
|
||
# --------------------------------------------------------
|
||
# Footer
|
||
# --------------------------------------------------------
|
||
|
||
html += page_end()
|
||
|
||
return html
|
||
|
||
# ============================================================
|
||
# All Capture Sessions
|
||
# ============================================================
|
||
|
||
def generate_capture_sessions(
|
||
conn,
|
||
context=None,
|
||
):
|
||
|
||
sessions = all_sessions(
|
||
conn
|
||
)
|
||
|
||
html = page_begin(
|
||
|
||
title="Capture Sessions",
|
||
|
||
description="All capture sessions",
|
||
|
||
active="capture_sessions",
|
||
|
||
context=context,
|
||
|
||
)
|
||
|
||
html += page_header(
|
||
|
||
"🚗 Capture Sessions",
|
||
|
||
f"""
|
||
|
||
Total sessions:
|
||
|
||
<b>{len(sessions)}</b>
|
||
|
||
"""
|
||
)
|
||
|
||
cards = []
|
||
|
||
for session in sessions:
|
||
|
||
cards.append(
|
||
session_card(
|
||
session,
|
||
context,
|
||
)
|
||
)
|
||
|
||
html += """
|
||
|
||
<div class="report-grid">
|
||
|
||
"""
|
||
|
||
html += "\n".join(cards)
|
||
|
||
html += """
|
||
|
||
</div>
|
||
|
||
"""
|
||
|
||
html += page_end()
|
||
|
||
return html |