2025-12-14 19:08:01 +01:00
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
Dashboard template for viewing honeypot statistics.
|
|
|
|
|
|
Customize this template to change the dashboard appearance.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2025-12-28 10:43:32 -06:00
|
|
|
|
import html
|
2025-12-28 17:07:18 +01:00
|
|
|
|
from datetime import datetime
|
2026-01-08 19:20:22 +01:00
|
|
|
|
from zoneinfo import ZoneInfo
|
2025-12-28 10:43:32 -06:00
|
|
|
|
|
2026-01-23 22:00:21 +01:00
|
|
|
|
|
2025-12-28 10:43:32 -06:00
|
|
|
|
def _escape(value) -> str:
|
|
|
|
|
|
"""Escape HTML special characters to prevent XSS attacks."""
|
|
|
|
|
|
if value is None:
|
|
|
|
|
|
return ""
|
|
|
|
|
|
return html.escape(str(value))
|
|
|
|
|
|
|
2026-01-23 22:00:21 +01:00
|
|
|
|
|
2026-01-17 18:06:09 +01:00
|
|
|
|
def format_timestamp(iso_timestamp: str, time_only: bool = False) -> str:
|
2026-01-08 19:20:22 +01:00
|
|
|
|
"""Format ISO timestamp for display with timezone conversion
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-08 19:20:22 +01:00
|
|
|
|
Args:
|
|
|
|
|
|
iso_timestamp: ISO format timestamp string (UTC)
|
|
|
|
|
|
time_only: If True, return only HH:MM:SS, otherwise full datetime
|
|
|
|
|
|
"""
|
2025-12-28 17:07:18 +01:00
|
|
|
|
try:
|
2026-01-08 19:20:22 +01:00
|
|
|
|
# Parse UTC timestamp
|
2025-12-28 17:07:18 +01:00
|
|
|
|
dt = datetime.fromisoformat(iso_timestamp)
|
2026-01-08 19:20:22 +01:00
|
|
|
|
if time_only:
|
|
|
|
|
|
return dt.strftime("%H:%M:%S")
|
2025-12-28 17:07:18 +01:00
|
|
|
|
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
# Fallback for old format
|
2026-01-23 22:00:21 +01:00
|
|
|
|
return (
|
|
|
|
|
|
iso_timestamp.split("T")[1][:8] if "T" in iso_timestamp else iso_timestamp
|
|
|
|
|
|
)
|
2025-12-28 17:07:18 +01:00
|
|
|
|
|
2025-12-14 19:08:01 +01:00
|
|
|
|
|
2026-01-23 22:00:21 +01:00
|
|
|
|
def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
|
2026-01-08 19:20:22 +01:00
|
|
|
|
"""Generate dashboard HTML with access statistics
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-08 19:20:22 +01:00
|
|
|
|
Args:
|
|
|
|
|
|
stats: Statistics dictionary
|
2026-01-09 20:37:20 +01:00
|
|
|
|
dashboard_path: The secret dashboard path for generating API URLs
|
2026-01-08 19:20:22 +01:00
|
|
|
|
"""
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-06 18:50:36 +01:00
|
|
|
|
# Generate suspicious accesses rows with clickable IPs
|
2026-01-23 22:00:21 +01:00
|
|
|
|
suspicious_rows = (
|
|
|
|
|
|
"\n".join([f"""<tr class="ip-row" data-ip="{_escape(log["ip"])}">
|
2026-01-06 18:50:36 +01:00
|
|
|
|
<td class="ip-clickable">{_escape(log["ip"])}</td>
|
|
|
|
|
|
<td>{_escape(log["path"])}</td>
|
|
|
|
|
|
<td style="word-break: break-all;">{_escape(log["user_agent"][:60])}</td>
|
2026-01-17 18:06:09 +01:00
|
|
|
|
<td>{format_timestamp(log["timestamp"], time_only=True)}</td>
|
2026-01-06 18:50:36 +01:00
|
|
|
|
</tr>
|
|
|
|
|
|
<tr class="ip-stats-row" id="stats-row-suspicious-{_escape(log["ip"]).replace(".", "-")}" style="display: none;">
|
|
|
|
|
|
<td colspan="4" class="ip-stats-cell">
|
|
|
|
|
|
<div class="ip-stats-dropdown">
|
|
|
|
|
|
<div class="loading">Loading stats...</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</td>
|
2026-01-23 22:00:21 +01:00
|
|
|
|
</tr>""" for log in stats["recent_suspicious"][-10:]])
|
|
|
|
|
|
or '<tr><td colspan="4" style="text-align:center;">No suspicious activity detected</td></tr>'
|
|
|
|
|
|
)
|
2025-12-14 19:08:01 +01:00
|
|
|
|
|
|
|
|
|
|
return f"""<!DOCTYPE html>
|
|
|
|
|
|
<html>
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
|
<title>Krawl Dashboard</title>
|
2026-01-29 11:55:06 +01:00
|
|
|
|
<link rel="icon" type="image/svg+xml" href="https://raw.githubusercontent.com/BlessedRebuS/Krawl/refs/heads/main/img/krawl-svg.svg" />
|
2026-01-25 22:50:27 +01:00
|
|
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css" />
|
|
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script>
|
|
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script>
|
2025-12-14 19:08:01 +01:00
|
|
|
|
<style>
|
|
|
|
|
|
body {{
|
|
|
|
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
|
|
|
|
background-color: #0d1117;
|
|
|
|
|
|
color: #c9d1d9;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.container {{
|
|
|
|
|
|
max-width: 1400px;
|
|
|
|
|
|
margin: 0 auto;
|
2026-01-09 20:37:20 +01:00
|
|
|
|
position: relative;
|
2025-12-14 19:08:01 +01:00
|
|
|
|
}}
|
2026-01-29 11:55:06 +01:00
|
|
|
|
.github-logo {{
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
|
color: #58a6ff;
|
|
|
|
|
|
transition: color 0.2s;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.github-logo:hover {{
|
|
|
|
|
|
color: #79c0ff;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.github-logo svg {{
|
|
|
|
|
|
width: 32px;
|
|
|
|
|
|
height: 32px;
|
|
|
|
|
|
fill: currentColor;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.github-logo-text {{
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
|
}}
|
2025-12-14 19:08:01 +01:00
|
|
|
|
h1 {{
|
|
|
|
|
|
color: #58a6ff;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
margin-bottom: 40px;
|
|
|
|
|
|
}}
|
2026-01-09 20:37:20 +01:00
|
|
|
|
.download-section {{
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.download-btn {{
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
padding: 8px 14px;
|
|
|
|
|
|
background: #238636;
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
transition: background 0.2s;
|
|
|
|
|
|
border: 1px solid #2ea043;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.download-btn:hover {{
|
|
|
|
|
|
background: #2ea043;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.download-btn:active {{
|
|
|
|
|
|
background: #1f7a2f;
|
|
|
|
|
|
}}
|
2025-12-14 19:08:01 +01:00
|
|
|
|
.stats-grid {{
|
|
|
|
|
|
display: grid;
|
2026-01-25 22:50:27 +01:00
|
|
|
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
2025-12-14 19:08:01 +01:00
|
|
|
|
gap: 20px;
|
|
|
|
|
|
margin-bottom: 40px;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.stat-card {{
|
|
|
|
|
|
background: #161b22;
|
|
|
|
|
|
border: 1px solid #30363d;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.stat-card.alert {{
|
|
|
|
|
|
border-color: #f85149;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.stat-value {{
|
|
|
|
|
|
font-size: 36px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
color: #58a6ff;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.stat-value.alert {{
|
|
|
|
|
|
color: #f85149;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.stat-label {{
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #8b949e;
|
|
|
|
|
|
margin-top: 5px;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.table-container {{
|
|
|
|
|
|
background: #161b22;
|
|
|
|
|
|
border: 1px solid #30363d;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
}}
|
|
|
|
|
|
h2 {{
|
|
|
|
|
|
color: #58a6ff;
|
|
|
|
|
|
margin-top: 0;
|
|
|
|
|
|
}}
|
|
|
|
|
|
table {{
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
border-collapse: collapse;
|
|
|
|
|
|
}}
|
|
|
|
|
|
th, td {{
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
border-bottom: 1px solid #30363d;
|
|
|
|
|
|
}}
|
|
|
|
|
|
th {{
|
|
|
|
|
|
background: #0d1117;
|
|
|
|
|
|
color: #58a6ff;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}}
|
|
|
|
|
|
tr:hover {{
|
|
|
|
|
|
background: #1c2128;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.rank {{
|
|
|
|
|
|
color: #8b949e;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.alert-section {{
|
|
|
|
|
|
background: #1c1917;
|
|
|
|
|
|
border-left: 4px solid #f85149;
|
|
|
|
|
|
}}
|
2026-01-05 17:27:27 +01:00
|
|
|
|
th.sortable {{
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
padding-right: 24px;
|
|
|
|
|
|
}}
|
|
|
|
|
|
th.sortable:hover {{
|
|
|
|
|
|
background: #1c2128;
|
|
|
|
|
|
}}
|
|
|
|
|
|
th.sortable::after {{
|
|
|
|
|
|
content: '⇅';
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
right: 8px;
|
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}}
|
|
|
|
|
|
th.sortable.asc::after {{
|
|
|
|
|
|
content: '▲';
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
}}
|
|
|
|
|
|
th.sortable.desc::after {{
|
|
|
|
|
|
content: '▼';
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
}}
|
2026-01-25 22:50:27 +01:00
|
|
|
|
tbody {{
|
|
|
|
|
|
transition: opacity 0.1s ease;
|
|
|
|
|
|
}}
|
|
|
|
|
|
tbody {{
|
|
|
|
|
|
animation: fadeIn 0.3s ease-in;
|
|
|
|
|
|
}}
|
2026-01-06 18:50:36 +01:00
|
|
|
|
.ip-row {{
|
|
|
|
|
|
transition: background-color 0.2s;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.ip-clickable {{
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
color: #58a6ff !important;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
text-decoration: underline;
|
|
|
|
|
|
text-decoration-style: dotted;
|
|
|
|
|
|
text-underline-offset: 3px;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.ip-clickable:hover {{
|
|
|
|
|
|
color: #79c0ff !important;
|
|
|
|
|
|
text-decoration-style: solid;
|
|
|
|
|
|
background: #1c2128;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.ip-stats-row {{
|
|
|
|
|
|
background: #0d1117;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.ip-stats-cell {{
|
|
|
|
|
|
padding: 0 !important;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.ip-stats-dropdown {{
|
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
|
padding: 15px;
|
|
|
|
|
|
background: #0d1117;
|
|
|
|
|
|
border: 1px solid #30363d;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 20px;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.stats-left {{
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.stats-right {{
|
|
|
|
|
|
flex: 0 0 200px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.radar-chart {{
|
|
|
|
|
|
position: relative;
|
2026-01-07 18:24:43 +01:00
|
|
|
|
width: 220px;
|
|
|
|
|
|
height: 220px;
|
2026-01-06 18:50:36 +01:00
|
|
|
|
overflow: visible;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.radar-legend {{
|
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.radar-legend-item {{
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
margin: 3px 0;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.radar-legend-color {{
|
|
|
|
|
|
width: 12px;
|
|
|
|
|
|
height: 12px;
|
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.ip-stats-dropdown .loading {{
|
|
|
|
|
|
color: #8b949e;
|
|
|
|
|
|
font-style: italic;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.stat-row {{
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
padding: 5px 0;
|
|
|
|
|
|
border-bottom: 1px solid #21262d;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.stat-row:last-child {{
|
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.stat-label-sm {{
|
|
|
|
|
|
color: #8b949e;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.stat-value-sm {{
|
|
|
|
|
|
color: #58a6ff;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.category-badge {{
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.category-attacker {{
|
|
|
|
|
|
background: #f851491a;
|
|
|
|
|
|
color: #f85149;
|
|
|
|
|
|
border: 1px solid #f85149;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.category-good-crawler {{
|
|
|
|
|
|
background: #3fb9501a;
|
|
|
|
|
|
color: #3fb950;
|
|
|
|
|
|
border: 1px solid #3fb950;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.category-bad-crawler {{
|
|
|
|
|
|
background: #f0883e1a;
|
|
|
|
|
|
color: #f0883e;
|
|
|
|
|
|
border: 1px solid #f0883e;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.category-regular-user {{
|
|
|
|
|
|
background: #58a6ff1a;
|
|
|
|
|
|
color: #58a6ff;
|
|
|
|
|
|
border: 1px solid #58a6ff;
|
|
|
|
|
|
}}
|
2026-01-08 19:20:22 +01:00
|
|
|
|
.category-unknown {{
|
|
|
|
|
|
background: #8b949e1a;
|
|
|
|
|
|
color: #8b949e;
|
|
|
|
|
|
border: 1px solid #8b949e;
|
|
|
|
|
|
}}
|
2026-01-17 22:41:19 +01:00
|
|
|
|
.timeline-section {{
|
2026-01-07 18:24:43 +01:00
|
|
|
|
margin-top: 15px;
|
|
|
|
|
|
padding-top: 15px;
|
|
|
|
|
|
border-top: 1px solid #30363d;
|
|
|
|
|
|
}}
|
2026-01-17 22:41:19 +01:00
|
|
|
|
.timeline-container {{
|
2026-01-10 20:00:33 +01:00
|
|
|
|
display: flex;
|
2026-01-17 22:41:19 +01:00
|
|
|
|
gap: 20px;
|
|
|
|
|
|
min-height: 200px;
|
2026-01-07 18:24:43 +01:00
|
|
|
|
}}
|
2026-01-17 22:41:19 +01:00
|
|
|
|
.timeline-column {{
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
overflow: auto;
|
|
|
|
|
|
max-height: 350px;
|
2026-01-07 18:24:43 +01:00
|
|
|
|
}}
|
2026-01-17 22:41:19 +01:00
|
|
|
|
.timeline-column:first-child {{
|
|
|
|
|
|
flex: 1.5;
|
2026-01-08 19:20:22 +01:00
|
|
|
|
}}
|
2026-01-17 22:41:19 +01:00
|
|
|
|
.timeline-column:last-child {{
|
|
|
|
|
|
flex: 1;
|
2026-01-07 18:24:43 +01:00
|
|
|
|
}}
|
2026-01-17 22:41:19 +01:00
|
|
|
|
.timeline-header {{
|
|
|
|
|
|
color: #58a6ff;
|
|
|
|
|
|
font-size: 13px;
|
2026-01-07 18:24:43 +01:00
|
|
|
|
font-weight: 600;
|
2026-01-17 22:41:19 +01:00
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
padding-bottom: 8px;
|
|
|
|
|
|
border-bottom: 1px solid #30363d;
|
2026-01-07 18:24:43 +01:00
|
|
|
|
}}
|
2026-01-17 22:41:19 +01:00
|
|
|
|
.reputation-title {{
|
2026-01-07 18:24:43 +01:00
|
|
|
|
color: #8b949e;
|
|
|
|
|
|
font-size: 11px;
|
2026-01-10 20:00:33 +01:00
|
|
|
|
font-weight: 600;
|
2026-01-17 22:41:19 +01:00
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
margin-bottom: 8px;
|
2026-01-10 20:00:33 +01:00
|
|
|
|
}}
|
|
|
|
|
|
.reputation-badge {{
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
2026-01-17 22:41:19 +01:00
|
|
|
|
gap: 3px;
|
2026-01-10 20:00:33 +01:00
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
|
background: #161b22;
|
|
|
|
|
|
border: 1px solid #f851494d;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: #f85149;
|
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
|
transition: all 0.2s;
|
2026-01-17 22:41:19 +01:00
|
|
|
|
margin-bottom: 6px;
|
|
|
|
|
|
margin-right: 6px;
|
|
|
|
|
|
white-space: nowrap;
|
2026-01-10 20:00:33 +01:00
|
|
|
|
}}
|
|
|
|
|
|
.reputation-badge:hover {{
|
|
|
|
|
|
background: #1c2128;
|
|
|
|
|
|
border-color: #f85149;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.reputation-clean {{
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
2026-01-17 22:41:19 +01:00
|
|
|
|
gap: 3px;
|
|
|
|
|
|
padding: 4px 8px;
|
2026-01-10 20:00:33 +01:00
|
|
|
|
background: #161b22;
|
|
|
|
|
|
border: 1px solid #3fb9504d;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: #3fb950;
|
2026-01-17 22:41:19 +01:00
|
|
|
|
margin-bottom: 6px;
|
2026-01-10 20:00:33 +01:00
|
|
|
|
}}
|
2026-01-17 22:41:19 +01:00
|
|
|
|
.timeline {{
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
padding-left: 28px;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.timeline::before {{
|
|
|
|
|
|
content: '';
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
left: 11px;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
bottom: 0;
|
|
|
|
|
|
width: 2px;
|
|
|
|
|
|
background: #30363d;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.timeline-item {{
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
padding-bottom: 12px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.timeline-item:last-child {{
|
|
|
|
|
|
padding-bottom: 0;
|
2026-01-10 20:00:33 +01:00
|
|
|
|
}}
|
2026-01-17 22:41:19 +01:00
|
|
|
|
.timeline-marker {{
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
left: -23px;
|
|
|
|
|
|
width: 14px;
|
|
|
|
|
|
height: 14px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
border: 2px solid #0d1117;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.timeline-marker.attacker {{ background: #f85149; }}
|
|
|
|
|
|
.timeline-marker.good-crawler {{ background: #3fb950; }}
|
|
|
|
|
|
.timeline-marker.bad-crawler {{ background: #f0883e; }}
|
|
|
|
|
|
.timeline-marker.regular-user {{ background: #58a6ff; }}
|
|
|
|
|
|
.timeline-marker.unknown {{ background: #8b949e; }}
|
2026-01-25 22:50:27 +01:00
|
|
|
|
.tabs-container {{
|
|
|
|
|
|
border-bottom: 1px solid #30363d;
|
|
|
|
|
|
margin-bottom: 30px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 2px;
|
|
|
|
|
|
background: #161b22;
|
|
|
|
|
|
border-radius: 6px 6px 0 0;
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
overflow-y: hidden;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.tab-button {{
|
|
|
|
|
|
padding: 12px 20px;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: #8b949e;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
border-bottom: 3px solid transparent;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
bottom: -1px;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.tab-button:hover {{
|
|
|
|
|
|
color: #c9d1d9;
|
|
|
|
|
|
background: #1c2128;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.tab-button.active {{
|
|
|
|
|
|
color: #58a6ff;
|
|
|
|
|
|
border-bottom-color: #58a6ff;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.tab-content {{
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.tab-content.active {{
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.ip-stats-table {{
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
border-collapse: collapse;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.ip-stats-table th, .ip-stats-table td {{
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
border-bottom: 1px solid #30363d;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.ip-stats-table th {{
|
|
|
|
|
|
background: #0d1117;
|
|
|
|
|
|
color: #58a6ff;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.ip-stats-table tr:hover {{
|
|
|
|
|
|
background: #1c2128;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.ip-detail-modal {{
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
background: rgba(0, 0, 0, 0.7);
|
|
|
|
|
|
z-index: 1000;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.ip-detail-modal.show {{
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.ip-detail-content {{
|
|
|
|
|
|
background: #161b22;
|
|
|
|
|
|
border: 1px solid #30363d;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 30px;
|
|
|
|
|
|
max-width: 900px;
|
|
|
|
|
|
max-height: 90vh;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.ip-detail-close {{
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 15px;
|
|
|
|
|
|
right: 15px;
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: #8b949e;
|
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
width: 30px;
|
|
|
|
|
|
height: 30px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.ip-detail-close:hover {{
|
|
|
|
|
|
color: #c9d1d9;
|
|
|
|
|
|
}}
|
|
|
|
|
|
#attacker-map {{
|
|
|
|
|
|
background: #0d1117 !important;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.leaflet-container {{
|
|
|
|
|
|
background: #0d1117 !important;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.leaflet-tile {{
|
|
|
|
|
|
filter: none;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.leaflet-popup-content-wrapper {{
|
2026-01-29 11:55:06 +01:00
|
|
|
|
background-color: #0d1117;
|
2026-01-25 22:50:27 +01:00
|
|
|
|
color: #c9d1d9;
|
|
|
|
|
|
border: 1px solid #30363d;
|
2026-01-29 11:55:06 +01:00
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.leaflet-popup-content {{
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
min-width: 280px;
|
2026-01-25 22:50:27 +01:00
|
|
|
|
}}
|
|
|
|
|
|
.leaflet-popup-content-wrapper a {{
|
|
|
|
|
|
color: #58a6ff;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.leaflet-popup-tip {{
|
2026-01-29 11:55:06 +01:00
|
|
|
|
background: #0d1117;
|
|
|
|
|
|
border: 1px solid #30363d;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.ip-detail-popup .leaflet-popup-content-wrapper {{
|
|
|
|
|
|
max-width: 340px !important;
|
2026-01-25 22:50:27 +01:00
|
|
|
|
}}
|
2026-01-27 16:56:34 +01:00
|
|
|
|
/* Remove the default leaflet icon background */
|
|
|
|
|
|
.ip-custom-marker {{
|
|
|
|
|
|
background: none !important;
|
|
|
|
|
|
border: none !important;
|
|
|
|
|
|
}}
|
2026-01-26 12:36:22 +01:00
|
|
|
|
.ip-marker {{
|
2026-01-25 22:50:27 +01:00
|
|
|
|
border: 2px solid #fff;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
cursor: pointer;
|
2026-01-27 16:56:34 +01:00
|
|
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.ip-marker:hover {{
|
|
|
|
|
|
transform: scale(1.15);
|
2026-01-25 22:50:27 +01:00
|
|
|
|
}}
|
2026-01-26 12:36:22 +01:00
|
|
|
|
.marker-attacker {{
|
|
|
|
|
|
background: #f85149;
|
|
|
|
|
|
box-shadow: 0 0 8px rgba(248, 81, 73, 0.8), inset 0 0 4px rgba(248, 81, 73, 0.5);
|
|
|
|
|
|
}}
|
2026-01-27 16:56:34 +01:00
|
|
|
|
.marker-attacker:hover {{
|
|
|
|
|
|
box-shadow: 0 0 15px rgba(248, 81, 73, 1), inset 0 0 6px rgba(248, 81, 73, 0.7);
|
|
|
|
|
|
}}
|
2026-01-26 12:36:22 +01:00
|
|
|
|
.marker-bad_crawler {{
|
|
|
|
|
|
background: #f0883e;
|
|
|
|
|
|
box-shadow: 0 0 8px rgba(240, 136, 62, 0.8), inset 0 0 4px rgba(240, 136, 62, 0.5);
|
|
|
|
|
|
}}
|
2026-01-27 16:56:34 +01:00
|
|
|
|
.marker-bad_crawler:hover {{
|
|
|
|
|
|
box-shadow: 0 0 15px rgba(240, 136, 62, 1), inset 0 0 6px rgba(240, 136, 62, 0.7);
|
|
|
|
|
|
}}
|
2026-01-26 12:36:22 +01:00
|
|
|
|
.marker-good_crawler {{
|
|
|
|
|
|
background: #3fb950;
|
|
|
|
|
|
box-shadow: 0 0 8px rgba(63, 185, 80, 0.8), inset 0 0 4px rgba(63, 185, 80, 0.5);
|
2026-01-25 22:50:27 +01:00
|
|
|
|
}}
|
2026-01-27 16:56:34 +01:00
|
|
|
|
.marker-good_crawler:hover {{
|
|
|
|
|
|
box-shadow: 0 0 15px rgba(63, 185, 80, 1), inset 0 0 6px rgba(63, 185, 80, 0.7);
|
|
|
|
|
|
}}
|
2026-01-26 12:36:22 +01:00
|
|
|
|
.marker-regular_user {{
|
|
|
|
|
|
background: #58a6ff;
|
|
|
|
|
|
box-shadow: 0 0 8px rgba(88, 166, 255, 0.8), inset 0 0 4px rgba(88, 166, 255, 0.5);
|
2026-01-25 22:50:27 +01:00
|
|
|
|
}}
|
2026-01-27 16:56:34 +01:00
|
|
|
|
.marker-regular_user:hover {{
|
|
|
|
|
|
box-shadow: 0 0 15px rgba(88, 166, 255, 1), inset 0 0 6px rgba(88, 166, 255, 0.7);
|
|
|
|
|
|
}}
|
2026-01-26 12:36:22 +01:00
|
|
|
|
.marker-unknown {{
|
|
|
|
|
|
background: #8b949e;
|
|
|
|
|
|
box-shadow: 0 0 8px rgba(139, 148, 158, 0.8), inset 0 0 4px rgba(139, 148, 158, 0.5);
|
2026-01-25 22:50:27 +01:00
|
|
|
|
}}
|
2026-01-27 16:56:34 +01:00
|
|
|
|
.marker-unknown:hover {{
|
|
|
|
|
|
box-shadow: 0 0 15px rgba(139, 148, 158, 1), inset 0 0 6px rgba(139, 148, 158, 0.7);
|
|
|
|
|
|
}}
|
2026-01-25 22:50:27 +01:00
|
|
|
|
.leaflet-bottom.leaflet-right {{
|
|
|
|
|
|
display: none !important;
|
|
|
|
|
|
}}
|
|
|
|
|
|
#attack-types-chart {{
|
|
|
|
|
|
max-height: 400px;
|
|
|
|
|
|
}}
|
2026-01-06 18:50:36 +01:00
|
|
|
|
|
2025-12-14 19:08:01 +01:00
|
|
|
|
</style>
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
|
|
|
<div class="container">
|
2026-01-29 11:55:06 +01:00
|
|
|
|
<a href="https://github.com/BlessedRebuS/Krawl" class="github-logo" target="_blank" rel="noopener noreferrer">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
|
|
|
|
|
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
<span class="github-logo-text">BlessedRebuS/Krawl</span>
|
|
|
|
|
|
</a>
|
2026-01-09 20:37:20 +01:00
|
|
|
|
<div class="download-section">
|
|
|
|
|
|
<a href="{dashboard_path}/api/download/malicious_ips.txt" class="download-btn" download>
|
|
|
|
|
|
Export Malicious IPs
|
|
|
|
|
|
</a>
|
|
|
|
|
|
</div>
|
2026-01-05 17:27:27 +01:00
|
|
|
|
<h1>Krawl Dashboard</h1>
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2025-12-14 19:08:01 +01:00
|
|
|
|
<div class="stats-grid">
|
|
|
|
|
|
<div class="stat-card">
|
|
|
|
|
|
<div class="stat-value">{stats['total_accesses']}</div>
|
|
|
|
|
|
<div class="stat-label">Total Accesses</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="stat-card">
|
|
|
|
|
|
<div class="stat-value">{stats['unique_ips']}</div>
|
|
|
|
|
|
<div class="stat-label">Unique IPs</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="stat-card">
|
|
|
|
|
|
<div class="stat-value">{stats['unique_paths']}</div>
|
|
|
|
|
|
<div class="stat-label">Unique Paths</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="stat-card alert">
|
|
|
|
|
|
<div class="stat-value alert">{stats['suspicious_accesses']}</div>
|
|
|
|
|
|
<div class="stat-label">Suspicious Accesses</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="stat-card alert">
|
|
|
|
|
|
<div class="stat-value alert">{stats.get('honeypot_ips', 0)}</div>
|
|
|
|
|
|
<div class="stat-label">Honeypot Caught</div>
|
|
|
|
|
|
</div>
|
2025-12-27 19:17:27 +01:00
|
|
|
|
<div class="stat-card alert">
|
|
|
|
|
|
<div class="stat-value alert">{len(stats.get('credential_attempts', []))}</div>
|
|
|
|
|
|
<div class="stat-label">Credentials Captured</div>
|
|
|
|
|
|
</div>
|
2026-01-25 22:50:27 +01:00
|
|
|
|
<div class="stat-card alert">
|
|
|
|
|
|
<div class="stat-value alert">{stats.get('unique_attackers', 0)}</div>
|
|
|
|
|
|
<div class="stat-label">Unique Attackers</div>
|
|
|
|
|
|
</div>
|
2025-12-14 19:08:01 +01:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-25 22:50:27 +01:00
|
|
|
|
<div class="tabs-container">
|
|
|
|
|
|
<a class="tab-button active" href="#overview">Overview</a>
|
|
|
|
|
|
<a class="tab-button" href="#ip-stats">Attacks</a>
|
2025-12-14 19:08:01 +01:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-25 22:50:27 +01:00
|
|
|
|
<div id="overview" class="tab-content active">
|
|
|
|
|
|
<div class="table-container alert-section">
|
2026-01-05 17:27:27 +01:00
|
|
|
|
<h2>Recent Suspicious Activity</h2>
|
2025-12-14 19:08:01 +01:00
|
|
|
|
<table>
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>IP Address</th>
|
|
|
|
|
|
<th>Path</th>
|
|
|
|
|
|
<th>User-Agent</th>
|
|
|
|
|
|
<th>Time</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
{suspicious_rows}
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-12-27 19:17:27 +01:00
|
|
|
|
<div class="table-container alert-section">
|
2026-01-25 22:50:27 +01:00
|
|
|
|
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px;">
|
|
|
|
|
|
<h2 style="margin: 0;">Honeypot Triggers by IP</h2>
|
|
|
|
|
|
<div class="pagination-controls" id="honeypot-pagination" style="display: flex; align-items: center; gap: 12px; padding: 0; background: transparent;">
|
|
|
|
|
|
<div style="display: flex; align-items: center; gap: 6px; color: #6e7681; font-weight: 400; font-size: 12px;">
|
|
|
|
|
|
<span>Page <span class="current-page">1</span>/<span class="total-pages">1</span></span>
|
|
|
|
|
|
<span style="color: #6e7681;">•</span>
|
|
|
|
|
|
<span><span class="total-records">0</span> total</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button class="pagination-btn" onclick="previousPage('honeypot')" style="padding: 6px 12px; background: #0969da; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: 500; font-size: 12px; transition: background 0.2s;">← Prev</button>
|
|
|
|
|
|
<button class="pagination-btn" onclick="nextPage('honeypot')" style="padding: 6px 12px; background: #0969da; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: 500; font-size: 12px; transition: background 0.2s;">Next →</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<table id="honeypot-table" class="overview-table">
|
2025-12-27 19:17:27 +01:00
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
2026-01-25 22:50:27 +01:00
|
|
|
|
<th>#</th>
|
2025-12-27 19:17:27 +01:00
|
|
|
|
<th>IP Address</th>
|
2026-01-25 22:50:27 +01:00
|
|
|
|
<th>Accessed Paths</th>
|
|
|
|
|
|
<th class="sortable" data-sort="count" data-table="honeypot">Count</th>
|
2025-12-27 19:17:27 +01:00
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
2026-01-25 22:50:27 +01:00
|
|
|
|
<tbody id="honeypot-tbody">
|
|
|
|
|
|
<tr><td colspan="4" style="text-align: center;">Loading...</td></tr>
|
2025-12-27 19:17:27 +01:00
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-25 22:50:27 +01:00
|
|
|
|
<div style="display: flex; gap: 20px; margin-bottom: 20px;">
|
|
|
|
|
|
<div class="table-container" style="flex: 1;">
|
|
|
|
|
|
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px;">
|
|
|
|
|
|
<h2 style="margin: 0;">Top IP Addresses</h2>
|
|
|
|
|
|
<div class="pagination-controls" id="top-ips-pagination" style="display: flex; align-items: center; gap: 12px; padding: 0; background: transparent;">
|
|
|
|
|
|
<div style="display: flex; align-items: center; gap: 6px; color: #6e7681; font-weight: 400; font-size: 12px;">
|
|
|
|
|
|
<span>Page <span class="current-page">1</span>/<span class="total-pages">1</span></span>
|
|
|
|
|
|
<span style="color: #6e7681;">•</span>
|
|
|
|
|
|
<span><span class="total-records">0</span> total</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button class="pagination-btn" onclick="previousPage('top-ips')" style="padding: 6px 12px; background: #0969da; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: 500; font-size: 12px; transition: background 0.2s;">← Prev</button>
|
|
|
|
|
|
<button class="pagination-btn" onclick="nextPage('top-ips')" style="padding: 6px 12px; background: #0969da; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: 500; font-size: 12px; transition: background 0.2s;">Next →</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<table id="top-ips-table" class="overview-table">
|
2025-12-24 10:25:00 -06:00
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
2026-01-25 22:50:27 +01:00
|
|
|
|
<th>#</th>
|
2025-12-24 10:25:00 -06:00
|
|
|
|
<th>IP Address</th>
|
2026-01-25 22:50:27 +01:00
|
|
|
|
<th class="sortable" data-sort="count" data-table="top-ips">Access Count</th>
|
2025-12-24 10:25:00 -06:00
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
2026-01-25 22:50:27 +01:00
|
|
|
|
<tbody id="top-ips-tbody">
|
|
|
|
|
|
<tr><td colspan="3" style="text-align: center;">Loading...</td></tr>
|
2025-12-24 10:25:00 -06:00
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-25 22:50:27 +01:00
|
|
|
|
<div class="table-container" style="flex: 1;">
|
|
|
|
|
|
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px;">
|
|
|
|
|
|
<h2 style="margin: 0;">Top User-Agents</h2>
|
|
|
|
|
|
<div class="pagination-controls" id="top-ua-pagination" style="display: flex; align-items: center; gap: 12px; padding: 0; background: transparent;">
|
|
|
|
|
|
<div style="display: flex; align-items: center; gap: 6px; color: #6e7681; font-weight: 400; font-size: 12px;">
|
|
|
|
|
|
<span>Page <span class="current-page">1</span>/<span class="total-pages">1</span></span>
|
|
|
|
|
|
<span style="color: #6e7681;">•</span>
|
|
|
|
|
|
<span><span class="total-records">0</span> total</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button class="pagination-btn" onclick="previousPage('top-ua')" style="padding: 6px 12px; background: #0969da; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: 500; font-size: 12px; transition: background 0.2s;">← Prev</button>
|
|
|
|
|
|
<button class="pagination-btn" onclick="nextPage('top-ua')" style="padding: 6px 12px; background: #0969da; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: 500; font-size: 12px; transition: background 0.2s;">Next →</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<table id="top-ua-table" class="overview-table">
|
2025-12-14 19:08:01 +01:00
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>#</th>
|
2026-01-25 22:50:27 +01:00
|
|
|
|
<th>User-Agent</th>
|
|
|
|
|
|
<th class="sortable" data-sort="count" data-table="top-ua">Count</th>
|
2025-12-14 19:08:01 +01:00
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
2026-01-25 22:50:27 +01:00
|
|
|
|
<tbody id="top-ua-tbody">
|
|
|
|
|
|
<tr><td colspan="3" style="text-align: center;">Loading...</td></tr>
|
2025-12-14 19:08:01 +01:00
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
2026-01-25 22:50:27 +01:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-12-14 19:08:01 +01:00
|
|
|
|
|
2026-01-25 22:50:27 +01:00
|
|
|
|
<div id="ip-stats" class="tab-content">
|
|
|
|
|
|
<div class="table-container" style="margin-bottom: 30px;">
|
2026-01-26 12:36:22 +01:00
|
|
|
|
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px;">
|
|
|
|
|
|
<h2 style="margin: 0;">IP Origins Map</h2>
|
|
|
|
|
|
<div style="display: flex; gap: 16px; align-items: center; flex-wrap: wrap;">
|
|
|
|
|
|
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; color: #c9d1d9; font-size: 13px;">
|
|
|
|
|
|
<input type="checkbox" id="filter-attacker" checked onchange="updateMapFilters()" style="cursor: pointer;">
|
2026-01-29 11:55:06 +01:00
|
|
|
|
<span style="color: #f85149;">Attackers</span>
|
2026-01-26 12:36:22 +01:00
|
|
|
|
</label>
|
|
|
|
|
|
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; color: #c9d1d9; font-size: 13px;">
|
|
|
|
|
|
<input type="checkbox" id="filter-bad-crawler" checked onchange="updateMapFilters()" style="cursor: pointer;">
|
2026-01-29 11:55:06 +01:00
|
|
|
|
<span style="color: #f0883e;">Bad Crawlers</span>
|
2026-01-26 12:36:22 +01:00
|
|
|
|
</label>
|
|
|
|
|
|
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; color: #c9d1d9; font-size: 13px;">
|
|
|
|
|
|
<input type="checkbox" id="filter-good-crawler" checked onchange="updateMapFilters()" style="cursor: pointer;">
|
2026-01-29 11:55:06 +01:00
|
|
|
|
<span style="color: #3fb950;">Good Crawlers</span>
|
2026-01-26 12:36:22 +01:00
|
|
|
|
</label>
|
|
|
|
|
|
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; color: #c9d1d9; font-size: 13px;">
|
|
|
|
|
|
<input type="checkbox" id="filter-regular-user" checked onchange="updateMapFilters()" style="cursor: pointer;">
|
2026-01-29 11:55:06 +01:00
|
|
|
|
<span style="color: #58a6ff;">Regular Users</span>
|
2026-01-26 12:36:22 +01:00
|
|
|
|
</label>
|
|
|
|
|
|
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; color: #c9d1d9; font-size: 13px;">
|
|
|
|
|
|
<input type="checkbox" id="filter-unknown" checked onchange="updateMapFilters()" style="cursor: pointer;">
|
2026-01-29 11:55:06 +01:00
|
|
|
|
<span style="color: #8b949e;">Unknown</span>
|
2026-01-26 12:36:22 +01:00
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-25 22:50:27 +01:00
|
|
|
|
<div id="attacker-map" style="height: 500px; border-radius: 6px; overflow: hidden; border: 1px solid #30363d; background: #161b22;">
|
|
|
|
|
|
<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #8b949e;">Loading map...</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="table-container alert-section" style="position: relative;">
|
|
|
|
|
|
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px;">
|
|
|
|
|
|
<h2 style="margin: 0;">Attackers by Total Requests</h2>
|
|
|
|
|
|
<div class="pagination-controls" style="display: flex; align-items: center; gap: 12px; padding: 0; background: transparent;">
|
|
|
|
|
|
<div style="display: flex; align-items: center; gap: 6px; color: #6e7681; font-weight: 400; font-size: 12px;">
|
|
|
|
|
|
<span>Page <span id="current-page">1</span>/<span id="total-pages">1</span></span>
|
|
|
|
|
|
<span style="color: #6e7681;">•</span>
|
|
|
|
|
|
<span><span id="total-attackers">0</span> total</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button id="prev-page-btn" class="pagination-btn" onclick="previousPageIpStats()" style="padding: 6px 12px; background: #0969da; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: 500; font-size: 12px; transition: background 0.2s;">← Prev</button>
|
|
|
|
|
|
<button id="next-page-btn" class="pagination-btn" onclick="nextPageIpStats()" style="padding: 6px 12px; background: #0969da; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: 500; font-size: 12px; transition: background 0.2s;">Next →</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<table class="ip-stats-table">
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>#</th>
|
|
|
|
|
|
<th>IP Address</th>
|
|
|
|
|
|
<th class="sortable" data-sort="total_requests">Total Requests</th>
|
|
|
|
|
|
<th>First Seen</th>
|
|
|
|
|
|
<th>Last Seen</th>
|
|
|
|
|
|
<th>Location</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody id="ip-stats-tbody">
|
|
|
|
|
|
<!-- Dynamically populated -->
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="table-container alert-section">
|
|
|
|
|
|
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px;">
|
|
|
|
|
|
<h2 style="margin: 0;">Captured Credentials</h2>
|
|
|
|
|
|
<div class="pagination-controls" id="credentials-pagination" style="display: flex; align-items: center; gap: 12px; padding: 0; background: transparent;">
|
|
|
|
|
|
<div style="display: flex; align-items: center; gap: 6px; color: #6e7681; font-weight: 400; font-size: 12px;">
|
|
|
|
|
|
<span>Page <span class="current-page">1</span>/<span class="total-pages">1</span></span>
|
|
|
|
|
|
<span style="color: #6e7681;">•</span>
|
|
|
|
|
|
<span><span class="total-records">0</span> total</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button class="pagination-btn" onclick="previousPage('credentials')" style="padding: 6px 12px; background: #0969da; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: 500; font-size: 12px; transition: background 0.2s;">← Prev</button>
|
|
|
|
|
|
<button class="pagination-btn" onclick="nextPage('credentials')" style="padding: 6px 12px; background: #0969da; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: 500; font-size: 12px; transition: background 0.2s;">Next →</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<table id="credentials-table" class="overview-table">
|
2025-12-14 19:08:01 +01:00
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>#</th>
|
2026-01-25 22:50:27 +01:00
|
|
|
|
<th>IP Address</th>
|
|
|
|
|
|
<th>Username</th>
|
|
|
|
|
|
<th>Password</th>
|
2025-12-14 19:08:01 +01:00
|
|
|
|
<th>Path</th>
|
2026-01-25 22:50:27 +01:00
|
|
|
|
<th class="sortable" data-sort="timestamp" data-table="credentials">Time</th>
|
2025-12-14 19:08:01 +01:00
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
2026-01-25 22:50:27 +01:00
|
|
|
|
<tbody id="credentials-tbody">
|
|
|
|
|
|
<tr><td colspan="6" style="text-align: center;">Loading...</td></tr>
|
2025-12-14 19:08:01 +01:00
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-25 22:50:27 +01:00
|
|
|
|
<div class="table-container alert-section">
|
|
|
|
|
|
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px;">
|
|
|
|
|
|
<h2 style="margin: 0;">Detected Attack Types</h2>
|
|
|
|
|
|
<div class="pagination-controls" id="attacks-pagination" style="display: flex; align-items: center; gap: 12px; padding: 0; background: transparent;">
|
|
|
|
|
|
<div style="display: flex; align-items: center; gap: 6px; color: #6e7681; font-weight: 400; font-size: 12px;">
|
|
|
|
|
|
<span>Page <span class="current-page">1</span>/<span class="total-pages">1</span></span>
|
|
|
|
|
|
<span style="color: #6e7681;">•</span>
|
|
|
|
|
|
<span><span class="total-records">0</span> total</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button class="pagination-btn" onclick="previousPage('attacks')" style="padding: 6px 12px; background: #0969da; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: 500; font-size: 12px; transition: background 0.2s;">← Prev</button>
|
|
|
|
|
|
<button class="pagination-btn" onclick="nextPage('attacks')" style="padding: 6px 12px; background: #0969da; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: 500; font-size: 12px; transition: background 0.2s;">Next →</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<table id="attacks-table" class="overview-table">
|
2025-12-14 19:08:01 +01:00
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>#</th>
|
2026-01-25 22:50:27 +01:00
|
|
|
|
<th>IP Address</th>
|
|
|
|
|
|
<th>Path</th>
|
|
|
|
|
|
<th>Attack Types</th>
|
2025-12-14 19:08:01 +01:00
|
|
|
|
<th>User-Agent</th>
|
2026-01-25 22:50:27 +01:00
|
|
|
|
<th class="sortable" data-sort="timestamp" data-table="attacks">Time</th>
|
2025-12-14 19:08:01 +01:00
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
2026-01-25 22:50:27 +01:00
|
|
|
|
<tbody id="attacks-tbody">
|
|
|
|
|
|
<tr><td colspan="6" style="text-align: center;">Loading...</td></tr>
|
2025-12-14 19:08:01 +01:00
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
2026-01-25 22:50:27 +01:00
|
|
|
|
|
|
|
|
|
|
<div class="table-container alert-section" style="margin-top: 20px;">
|
2026-01-29 11:55:06 +01:00
|
|
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
|
|
|
|
|
<h2 style="margin: 0;">Most Recurring Attack Types</h2>
|
|
|
|
|
|
<div style="font-size: 12px; color: #8b949e;">Top 10 Attack Vectors</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style="position: relative; height: 450px; margin-top: 20px;">
|
2026-01-25 22:50:27 +01:00
|
|
|
|
<canvas id="attack-types-chart"></canvas>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div id="ip-detail-modal" class="ip-detail-modal">
|
|
|
|
|
|
<div class="ip-detail-content">
|
|
|
|
|
|
<button class="ip-detail-close" onclick="closeIpDetailModal()">×</button>
|
|
|
|
|
|
<div id="ip-detail-body">
|
|
|
|
|
|
<!-- Dynamically populated -->
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-12-14 19:08:01 +01:00
|
|
|
|
</div>
|
2026-01-05 17:27:27 +01:00
|
|
|
|
<script>
|
2026-01-09 20:37:20 +01:00
|
|
|
|
const DASHBOARD_PATH = '{dashboard_path}';
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-08 19:20:22 +01:00
|
|
|
|
function formatTimestamp(isoTimestamp) {{
|
|
|
|
|
|
if (!isoTimestamp) return 'N/A';
|
|
|
|
|
|
try {{
|
|
|
|
|
|
const date = new Date(isoTimestamp);
|
2026-01-17 18:06:09 +01:00
|
|
|
|
return date.toLocaleString('en-US', {{
|
2026-01-08 19:20:22 +01:00
|
|
|
|
year: 'numeric',
|
|
|
|
|
|
month: '2-digit',
|
|
|
|
|
|
day: '2-digit',
|
|
|
|
|
|
hour: '2-digit',
|
|
|
|
|
|
minute: '2-digit',
|
|
|
|
|
|
second: '2-digit',
|
|
|
|
|
|
hour12: false
|
|
|
|
|
|
}});
|
|
|
|
|
|
}} catch (err) {{
|
|
|
|
|
|
console.error('Error formatting timestamp:', err);
|
|
|
|
|
|
return new Date(isoTimestamp).toLocaleString();
|
|
|
|
|
|
}}
|
|
|
|
|
|
}}
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-05 17:27:27 +01:00
|
|
|
|
document.querySelectorAll('th.sortable').forEach(header => {{
|
|
|
|
|
|
header.addEventListener('click', function() {{
|
|
|
|
|
|
const table = this.closest('table');
|
|
|
|
|
|
const tbody = table.querySelector('tbody');
|
|
|
|
|
|
const rows = Array.from(tbody.querySelectorAll('tr'));
|
|
|
|
|
|
const sortType = this.getAttribute('data-sort');
|
|
|
|
|
|
const columnIndex = Array.from(this.parentElement.children).indexOf(this);
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-05 17:27:27 +01:00
|
|
|
|
const isAscending = this.classList.contains('asc');
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-05 17:27:27 +01:00
|
|
|
|
table.querySelectorAll('th.sortable').forEach(th => {{
|
|
|
|
|
|
th.classList.remove('asc', 'desc');
|
|
|
|
|
|
}});
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-05 17:27:27 +01:00
|
|
|
|
this.classList.add(isAscending ? 'desc' : 'asc');
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-05 17:27:27 +01:00
|
|
|
|
rows.sort((a, b) => {{
|
|
|
|
|
|
let aValue = a.cells[columnIndex].textContent.trim();
|
|
|
|
|
|
let bValue = b.cells[columnIndex].textContent.trim();
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-05 17:27:27 +01:00
|
|
|
|
if (sortType === 'count') {{
|
|
|
|
|
|
aValue = parseInt(aValue) || 0;
|
|
|
|
|
|
bValue = parseInt(bValue) || 0;
|
|
|
|
|
|
return isAscending ? bValue - aValue : aValue - bValue;
|
|
|
|
|
|
}}
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-05 17:27:27 +01:00
|
|
|
|
if (sortType === 'ip') {{
|
|
|
|
|
|
const ipToNum = ip => {{
|
|
|
|
|
|
const parts = ip.split('.');
|
|
|
|
|
|
if (parts.length !== 4) return 0;
|
|
|
|
|
|
return parts.reduce((acc, part, i) => acc + (parseInt(part) || 0) * Math.pow(256, 3 - i), 0);
|
|
|
|
|
|
}};
|
|
|
|
|
|
const aNum = ipToNum(aValue);
|
|
|
|
|
|
const bNum = ipToNum(bValue);
|
|
|
|
|
|
return isAscending ? bNum - aNum : aNum - bNum;
|
|
|
|
|
|
}}
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-05 17:27:27 +01:00
|
|
|
|
if (isAscending) {{
|
|
|
|
|
|
return bValue.localeCompare(aValue);
|
|
|
|
|
|
}} else {{
|
|
|
|
|
|
return aValue.localeCompare(bValue);
|
|
|
|
|
|
}}
|
|
|
|
|
|
}});
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-05 17:27:27 +01:00
|
|
|
|
rows.forEach(row => tbody.appendChild(row));
|
|
|
|
|
|
}});
|
|
|
|
|
|
}});
|
2026-01-06 18:50:36 +01:00
|
|
|
|
|
|
|
|
|
|
document.querySelectorAll('.ip-clickable').forEach(cell => {{
|
|
|
|
|
|
cell.addEventListener('click', async function(e) {{
|
|
|
|
|
|
const row = e.currentTarget.closest('.ip-row');
|
|
|
|
|
|
if (!row) return;
|
|
|
|
|
|
|
|
|
|
|
|
const ip = row.getAttribute('data-ip');
|
|
|
|
|
|
const statsRow = row.nextElementSibling;
|
|
|
|
|
|
if (!statsRow || !statsRow.classList.contains('ip-stats-row')) return;
|
|
|
|
|
|
|
|
|
|
|
|
const isVisible = getComputedStyle(statsRow).display !== 'none';
|
|
|
|
|
|
|
|
|
|
|
|
document.querySelectorAll('.ip-stats-row').forEach(r => {{
|
|
|
|
|
|
r.style.display = 'none';
|
|
|
|
|
|
}});
|
|
|
|
|
|
|
|
|
|
|
|
if (isVisible) return;
|
|
|
|
|
|
|
|
|
|
|
|
statsRow.style.display = 'table-row';
|
|
|
|
|
|
|
|
|
|
|
|
const dropdown = statsRow.querySelector('.ip-stats-dropdown');
|
|
|
|
|
|
|
|
|
|
|
|
if (dropdown) {{
|
|
|
|
|
|
dropdown.innerHTML = '<div class="loading">Loading stats...</div>';
|
|
|
|
|
|
try {{
|
2026-01-09 20:37:20 +01:00
|
|
|
|
const response = await fetch(`${{DASHBOARD_PATH}}/api/ip-stats/${{ip}}`, {{
|
2026-01-06 18:50:36 +01:00
|
|
|
|
cache: 'no-store',
|
|
|
|
|
|
headers: {{
|
|
|
|
|
|
'Cache-Control': 'no-cache',
|
|
|
|
|
|
'Pragma': 'no-cache'
|
|
|
|
|
|
}}
|
|
|
|
|
|
}});
|
|
|
|
|
|
if (!response.ok) throw new Error(`HTTP ${{response.status}}`);
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
dropdown.innerHTML = data.error
|
|
|
|
|
|
? `<div style="color:#f85149;">Error: ${{data.error}}</div>`
|
|
|
|
|
|
: formatIpStats(data);
|
|
|
|
|
|
}} catch (err) {{
|
|
|
|
|
|
dropdown.innerHTML = `<div style="color:#f85149;">Failed to load stats: ${{err.message}}</div>`;
|
|
|
|
|
|
}}
|
|
|
|
|
|
}}
|
|
|
|
|
|
}});
|
|
|
|
|
|
}});
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-06 18:50:36 +01:00
|
|
|
|
function formatIpStats(stats) {{
|
|
|
|
|
|
let html = '<div class="stats-left">';
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-06 18:50:36 +01:00
|
|
|
|
html += '<div class="stat-row">';
|
|
|
|
|
|
html += '<span class="stat-label-sm">Total Requests:</span>';
|
|
|
|
|
|
html += `<span class="stat-value-sm">${{stats.total_requests || 0}}</span>`;
|
|
|
|
|
|
html += '</div>';
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-06 18:50:36 +01:00
|
|
|
|
html += '<div class="stat-row">';
|
|
|
|
|
|
html += '<span class="stat-label-sm">First Seen:</span>';
|
2026-01-08 19:20:22 +01:00
|
|
|
|
html += `<span class="stat-value-sm">${{formatTimestamp(stats.first_seen)}}</span>`;
|
2026-01-06 18:50:36 +01:00
|
|
|
|
html += '</div>';
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-06 18:50:36 +01:00
|
|
|
|
html += '<div class="stat-row">';
|
|
|
|
|
|
html += '<span class="stat-label-sm">Last Seen:</span>';
|
2026-01-08 19:20:22 +01:00
|
|
|
|
html += `<span class="stat-value-sm">${{formatTimestamp(stats.last_seen)}}</span>`;
|
2026-01-06 18:50:36 +01:00
|
|
|
|
html += '</div>';
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-06 18:50:36 +01:00
|
|
|
|
if (stats.country_code || stats.city) {{
|
|
|
|
|
|
html += '<div class="stat-row">';
|
|
|
|
|
|
html += '<span class="stat-label-sm">Location:</span>';
|
2026-01-27 16:56:34 +01:00
|
|
|
|
html += `<span class="stat-value-sm">${{stats.city ? (stats.country_code ? `${{stats.city}}, ${{stats.country_code}}` : stats.city) : (stats.country_code || 'Unknown')}}</span>`;
|
2026-01-06 18:50:36 +01:00
|
|
|
|
html += '</div>';
|
|
|
|
|
|
}}
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-06 18:50:36 +01:00
|
|
|
|
if (stats.asn_org) {{
|
|
|
|
|
|
html += '<div class="stat-row">';
|
|
|
|
|
|
html += '<span class="stat-label-sm">ASN Org:</span>';
|
|
|
|
|
|
html += `<span class="stat-value-sm">${{stats.asn_org}}</span>`;
|
|
|
|
|
|
html += '</div>';
|
|
|
|
|
|
}}
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-06 18:50:36 +01:00
|
|
|
|
if (stats.reputation_score !== null && stats.reputation_score !== undefined) {{
|
|
|
|
|
|
html += '<div class="stat-row">';
|
|
|
|
|
|
html += '<span class="stat-label-sm">Reputation Score:</span>';
|
|
|
|
|
|
html += `<span class="stat-value-sm">${{stats.reputation_score}} ${{stats.reputation_source ? '(' + stats.reputation_source + ')' : ''}}</span>`;
|
|
|
|
|
|
html += '</div>';
|
|
|
|
|
|
}}
|
2026-01-10 20:00:33 +01:00
|
|
|
|
|
|
|
|
|
|
if (stats.category) {{
|
|
|
|
|
|
html += '<div class="stat-row">';
|
|
|
|
|
|
html += '<span class="stat-label-sm">Category:</span>';
|
|
|
|
|
|
const categoryClass = 'category-' + stats.category.toLowerCase().replace('_', '-');
|
|
|
|
|
|
html += `<span class="category-badge ${{categoryClass}}">${{stats.category}}</span>`;
|
|
|
|
|
|
html += '</div>';
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
2026-01-07 18:24:43 +01:00
|
|
|
|
if (stats.category_history && stats.category_history.length > 0) {{
|
2026-01-17 22:41:19 +01:00
|
|
|
|
html += '<div class="timeline-section">';
|
2026-01-07 18:24:43 +01:00
|
|
|
|
html += '<div class="timeline-container">';
|
2026-01-10 20:00:33 +01:00
|
|
|
|
|
2026-01-17 22:41:19 +01:00
|
|
|
|
// Timeline column
|
|
|
|
|
|
html += '<div class="timeline-column">';
|
|
|
|
|
|
html += '<div class="timeline-header">Behavior Timeline</div>';
|
2026-01-07 18:24:43 +01:00
|
|
|
|
html += '<div class="timeline">';
|
2026-01-10 20:00:33 +01:00
|
|
|
|
|
2026-01-17 22:41:19 +01:00
|
|
|
|
stats.category_history.forEach(change => {{
|
2026-01-07 18:24:43 +01:00
|
|
|
|
const categoryClass = change.new_category.toLowerCase().replace('_', '-');
|
2026-01-08 19:20:22 +01:00
|
|
|
|
const timestamp = formatTimestamp(change.timestamp);
|
2026-01-17 22:41:19 +01:00
|
|
|
|
const oldClass = change.old_category ? 'category-' + change.old_category.toLowerCase().replace('_', '-') : '';
|
|
|
|
|
|
const newClass = 'category-' + categoryClass;
|
|
|
|
|
|
|
2026-01-07 18:24:43 +01:00
|
|
|
|
html += '<div class="timeline-item">';
|
|
|
|
|
|
html += `<div class="timeline-marker ${{categoryClass}}"></div>`;
|
|
|
|
|
|
html += '<div class="timeline-content">';
|
2026-01-17 22:41:19 +01:00
|
|
|
|
|
2026-01-07 18:24:43 +01:00
|
|
|
|
if (change.old_category) {{
|
2026-01-17 22:41:19 +01:00
|
|
|
|
html += `<span class="category-badge ${{oldClass}}">${{change.old_category}}</span>`;
|
|
|
|
|
|
html += '<span style="color: #8b949e; margin: 0 4px;">→</span>';
|
2026-01-07 18:24:43 +01:00
|
|
|
|
}} else {{
|
2026-01-17 22:41:19 +01:00
|
|
|
|
html += '<span style="color: #8b949e;">Initial:</span>';
|
2026-01-07 18:24:43 +01:00
|
|
|
|
}}
|
2026-01-17 22:41:19 +01:00
|
|
|
|
|
|
|
|
|
|
html += `<span class="category-badge ${{newClass}}">${{change.new_category}}</span>`;
|
|
|
|
|
|
html += `<div class="timeline-time">${{timestamp}}</div>`;
|
2026-01-07 18:24:43 +01:00
|
|
|
|
html += '</div>';
|
|
|
|
|
|
html += '</div>';
|
|
|
|
|
|
}});
|
2026-01-10 20:00:33 +01:00
|
|
|
|
|
2026-01-07 18:24:43 +01:00
|
|
|
|
html += '</div>';
|
|
|
|
|
|
html += '</div>';
|
2026-01-17 22:41:19 +01:00
|
|
|
|
|
|
|
|
|
|
// Reputation column
|
|
|
|
|
|
html += '<div class="timeline-column">';
|
|
|
|
|
|
|
|
|
|
|
|
if (stats.list_on && Object.keys(stats.list_on).length > 0) {{
|
|
|
|
|
|
html += '<div class="timeline-header">Listed On</div>';
|
|
|
|
|
|
const sortedSources = Object.entries(stats.list_on).sort((a, b) => a[0].localeCompare(b[0]));
|
|
|
|
|
|
|
|
|
|
|
|
sortedSources.forEach(([source, url]) => {{
|
|
|
|
|
|
if (url && url !== 'N/A') {{
|
|
|
|
|
|
html += `<a href="${{url}}" target="_blank" rel="noopener noreferrer" class="reputation-badge" title="${{source}}">${{source}}</a>`;
|
|
|
|
|
|
}} else {{
|
|
|
|
|
|
html += `<span class="reputation-badge">${{source}}</span>`;
|
|
|
|
|
|
}}
|
|
|
|
|
|
}});
|
|
|
|
|
|
}} else if (stats.country_code || stats.asn) {{
|
|
|
|
|
|
html += '<div class="timeline-header">Reputation</div>';
|
|
|
|
|
|
html += '<span class="reputation-clean" title="Not found on public blacklists">✓ Clean</span>';
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
html += '</div>';
|
|
|
|
|
|
html += '</div>';
|
|
|
|
|
|
html += '</div>';
|
2026-01-07 18:24:43 +01:00
|
|
|
|
}}
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-06 18:50:36 +01:00
|
|
|
|
html += '</div>';
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-06 18:50:36 +01:00
|
|
|
|
if (stats.category_scores && Object.keys(stats.category_scores).length > 0) {{
|
|
|
|
|
|
html += '<div class="stats-right">';
|
2026-01-07 18:24:43 +01:00
|
|
|
|
html += '<div style="font-size: 13px; font-weight: 600; color: #58a6ff; margin-bottom: 10px;">Category Score</div>';
|
2026-01-06 18:50:36 +01:00
|
|
|
|
html += '<svg class="radar-chart" viewBox="-30 -30 260 260" preserveAspectRatio="xMidYMid meet">';
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-06 18:50:36 +01:00
|
|
|
|
const scores = {{
|
|
|
|
|
|
attacker: stats.category_scores.attacker || 0,
|
|
|
|
|
|
good_crawler: stats.category_scores.good_crawler || 0,
|
|
|
|
|
|
bad_crawler: stats.category_scores.bad_crawler || 0,
|
2026-01-08 19:20:22 +01:00
|
|
|
|
regular_user: stats.category_scores.regular_user || 0,
|
|
|
|
|
|
unknown: stats.category_scores.unknown || 0
|
2026-01-06 18:50:36 +01:00
|
|
|
|
}};
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-06 18:50:36 +01:00
|
|
|
|
const maxScore = Math.max(...Object.values(scores), 1);
|
2026-01-10 20:00:33 +01:00
|
|
|
|
const minVisibleRadius = 0.15;
|
2026-01-06 18:50:36 +01:00
|
|
|
|
const normalizedScores = {{}};
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-06 18:50:36 +01:00
|
|
|
|
Object.keys(scores).forEach(key => {{
|
|
|
|
|
|
normalizedScores[key] = minVisibleRadius + (scores[key] / maxScore) * (1 - minVisibleRadius);
|
|
|
|
|
|
}});
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-06 18:50:36 +01:00
|
|
|
|
const colors = {{
|
|
|
|
|
|
attacker: '#f85149',
|
|
|
|
|
|
good_crawler: '#3fb950',
|
|
|
|
|
|
bad_crawler: '#f0883e',
|
2026-01-08 19:20:22 +01:00
|
|
|
|
regular_user: '#58a6ff',
|
|
|
|
|
|
unknown: '#8b949e'
|
2026-01-06 18:50:36 +01:00
|
|
|
|
}};
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-06 18:50:36 +01:00
|
|
|
|
const labels = {{
|
|
|
|
|
|
attacker: 'Attacker',
|
|
|
|
|
|
good_crawler: 'Good Bot',
|
|
|
|
|
|
bad_crawler: 'Bad Bot',
|
2026-01-08 19:20:22 +01:00
|
|
|
|
regular_user: 'User',
|
|
|
|
|
|
unknown: 'Unknown'
|
2026-01-06 18:50:36 +01:00
|
|
|
|
}};
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-06 18:50:36 +01:00
|
|
|
|
const cx = 100, cy = 100, maxRadius = 75;
|
|
|
|
|
|
for (let i = 1; i <= 5; i++) {{
|
|
|
|
|
|
const r = (maxRadius / 5) * i;
|
|
|
|
|
|
html += `<circle cx="${{cx}}" cy="${{cy}}" r="${{r}}" fill="none" stroke="#30363d" stroke-width="0.5"/>`;
|
|
|
|
|
|
}}
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-08 19:20:22 +01:00
|
|
|
|
const angles = [0, 72, 144, 216, 288];
|
|
|
|
|
|
const keys = ['good_crawler', 'regular_user', 'unknown', 'bad_crawler', 'attacker'];
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-06 18:50:36 +01:00
|
|
|
|
angles.forEach((angle, i) => {{
|
|
|
|
|
|
const rad = (angle - 90) * Math.PI / 180;
|
|
|
|
|
|
const x2 = cx + maxRadius * Math.cos(rad);
|
|
|
|
|
|
const y2 = cy + maxRadius * Math.sin(rad);
|
|
|
|
|
|
html += `<line x1="${{cx}}" y1="${{cy}}" x2="${{x2}}" y2="${{y2}}" stroke="#30363d" stroke-width="0.5"/>`;
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-07 18:24:43 +01:00
|
|
|
|
const labelDist = maxRadius + 35;
|
2026-01-06 18:50:36 +01:00
|
|
|
|
const lx = cx + labelDist * Math.cos(rad);
|
|
|
|
|
|
const ly = cy + labelDist * Math.sin(rad);
|
|
|
|
|
|
html += `<text x="${{lx}}" y="${{ly}}" fill="#8b949e" font-size="12" text-anchor="middle" dominant-baseline="middle">${{labels[keys[i]]}}</text>`;
|
|
|
|
|
|
}});
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-06 18:50:36 +01:00
|
|
|
|
let points = [];
|
|
|
|
|
|
angles.forEach((angle, i) => {{
|
|
|
|
|
|
const normalizedScore = normalizedScores[keys[i]];
|
|
|
|
|
|
const rad = (angle - 90) * Math.PI / 180;
|
|
|
|
|
|
const r = normalizedScore * maxRadius;
|
|
|
|
|
|
const x = cx + r * Math.cos(rad);
|
|
|
|
|
|
const y = cy + r * Math.sin(rad);
|
|
|
|
|
|
points.push(`${{x}},${{y}}`);
|
|
|
|
|
|
}});
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-06 18:50:36 +01:00
|
|
|
|
const dominantKey = Object.keys(scores).reduce((a, b) => scores[a] > scores[b] ? a : b);
|
|
|
|
|
|
const dominantColor = colors[dominantKey];
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-06 18:50:36 +01:00
|
|
|
|
html += `<polygon points="${{points.join(' ')}}" fill="${{dominantColor}}" fill-opacity="0.4" stroke="${{dominantColor}}" stroke-width="2.5"/>`;
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-06 18:50:36 +01:00
|
|
|
|
angles.forEach((angle, i) => {{
|
|
|
|
|
|
const normalizedScore = normalizedScores[keys[i]];
|
|
|
|
|
|
const rad = (angle - 90) * Math.PI / 180;
|
|
|
|
|
|
const r = normalizedScore * maxRadius;
|
|
|
|
|
|
const x = cx + r * Math.cos(rad);
|
|
|
|
|
|
const y = cy + r * Math.sin(rad);
|
|
|
|
|
|
html += `<circle cx="${{x}}" cy="${{y}}" r="4.5" fill="${{colors[keys[i]]}}" stroke="#0d1117" stroke-width="2"/>`;
|
|
|
|
|
|
}});
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-06 18:50:36 +01:00
|
|
|
|
html += '</svg>';
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-06 18:50:36 +01:00
|
|
|
|
html += '<div class="radar-legend">';
|
|
|
|
|
|
keys.forEach(key => {{
|
|
|
|
|
|
html += '<div class="radar-legend-item">';
|
|
|
|
|
|
html += `<div class="radar-legend-color" style="background: ${{colors[key]}};"></div>`;
|
2026-01-07 18:24:43 +01:00
|
|
|
|
html += `<span style="color: #8b949e;">${{labels[key]}}: ${{scores[key]}} pt</span>`;
|
2026-01-06 18:50:36 +01:00
|
|
|
|
html += '</div>';
|
|
|
|
|
|
}});
|
|
|
|
|
|
html += '</div>';
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-06 18:50:36 +01:00
|
|
|
|
html += '</div>';
|
|
|
|
|
|
}}
|
2026-01-17 18:06:09 +01:00
|
|
|
|
|
2026-01-06 18:50:36 +01:00
|
|
|
|
return html;
|
|
|
|
|
|
}}
|
2026-01-25 22:50:27 +01:00
|
|
|
|
|
2026-01-29 11:55:06 +01:00
|
|
|
|
// Generate radar chart for map panel
|
|
|
|
|
|
function generateMapPanelRadarChart(categoryScores) {{
|
|
|
|
|
|
if (!categoryScores || Object.keys(categoryScores).length === 0) {{
|
|
|
|
|
|
return '<div style="color: #8b949e; text-align: center; padding: 20px;">No category data available</div>';
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
let html = '<div style="display: flex; flex-direction: column; align-items: center;">';
|
|
|
|
|
|
html += '<svg class="radar-chart" viewBox="-30 -30 260 260" preserveAspectRatio="xMidYMid meet" style="width: 160px; height: 160px;">';
|
|
|
|
|
|
|
|
|
|
|
|
const scores = {{
|
|
|
|
|
|
attacker: categoryScores.attacker || 0,
|
|
|
|
|
|
good_crawler: categoryScores.good_crawler || 0,
|
|
|
|
|
|
bad_crawler: categoryScores.bad_crawler || 0,
|
|
|
|
|
|
regular_user: categoryScores.regular_user || 0,
|
|
|
|
|
|
unknown: categoryScores.unknown || 0
|
|
|
|
|
|
}};
|
|
|
|
|
|
|
|
|
|
|
|
const maxScore = Math.max(...Object.values(scores), 1);
|
|
|
|
|
|
const minVisibleRadius = 0.15;
|
|
|
|
|
|
const normalizedScores = {{}};
|
|
|
|
|
|
|
|
|
|
|
|
Object.keys(scores).forEach(key => {{
|
|
|
|
|
|
normalizedScores[key] = minVisibleRadius + (scores[key] / maxScore) * (1 - minVisibleRadius);
|
|
|
|
|
|
}});
|
|
|
|
|
|
|
|
|
|
|
|
const colors = {{
|
|
|
|
|
|
attacker: '#f85149',
|
|
|
|
|
|
good_crawler: '#3fb950',
|
|
|
|
|
|
bad_crawler: '#f0883e',
|
|
|
|
|
|
regular_user: '#58a6ff',
|
|
|
|
|
|
unknown: '#8b949e'
|
|
|
|
|
|
}};
|
|
|
|
|
|
|
|
|
|
|
|
const labels = {{
|
|
|
|
|
|
attacker: 'Attacker',
|
|
|
|
|
|
good_crawler: 'Good Bot',
|
|
|
|
|
|
bad_crawler: 'Bad Bot',
|
|
|
|
|
|
regular_user: 'User',
|
|
|
|
|
|
unknown: 'Unknown'
|
|
|
|
|
|
}};
|
|
|
|
|
|
|
|
|
|
|
|
const cx = 100, cy = 100, maxRadius = 75;
|
|
|
|
|
|
for (let i = 1; i <= 5; i++) {{
|
|
|
|
|
|
const r = (maxRadius / 5) * i;
|
|
|
|
|
|
html += `<circle cx="${{cx}}" cy="${{cy}}" r="${{r}}" fill="none" stroke="#30363d" stroke-width="0.5"/>`;
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
const angles = [0, 72, 144, 216, 288];
|
|
|
|
|
|
const keys = ['good_crawler', 'regular_user', 'unknown', 'bad_crawler', 'attacker'];
|
|
|
|
|
|
|
|
|
|
|
|
angles.forEach((angle, i) => {{
|
|
|
|
|
|
const rad = (angle - 90) * Math.PI / 180;
|
|
|
|
|
|
const x2 = cx + maxRadius * Math.cos(rad);
|
|
|
|
|
|
const y2 = cy + maxRadius * Math.sin(rad);
|
|
|
|
|
|
html += `<line x1="${{cx}}" y1="${{cy}}" x2="${{x2}}" y2="${{y2}}" stroke="#30363d" stroke-width="0.5"/>`;
|
|
|
|
|
|
|
|
|
|
|
|
const labelDist = maxRadius + 35;
|
|
|
|
|
|
const lx = cx + labelDist * Math.cos(rad);
|
|
|
|
|
|
const ly = cy + labelDist * Math.sin(rad);
|
|
|
|
|
|
html += `<text x="${{lx}}" y="${{ly}}" fill="#8b949e" font-size="12" text-anchor="middle" dominant-baseline="middle">${{labels[keys[i]]}}</text>`;
|
|
|
|
|
|
}});
|
|
|
|
|
|
|
|
|
|
|
|
let points = [];
|
|
|
|
|
|
angles.forEach((angle, i) => {{
|
|
|
|
|
|
const normalizedScore = normalizedScores[keys[i]];
|
|
|
|
|
|
const rad = (angle - 90) * Math.PI / 180;
|
|
|
|
|
|
const r = normalizedScore * maxRadius;
|
|
|
|
|
|
const x = cx + r * Math.cos(rad);
|
|
|
|
|
|
const y = cy + r * Math.sin(rad);
|
|
|
|
|
|
points.push(`${{x}},${{y}}`);
|
|
|
|
|
|
}});
|
|
|
|
|
|
|
|
|
|
|
|
const dominantKey = Object.keys(scores).reduce((a, b) => scores[a] > scores[b] ? a : b);
|
|
|
|
|
|
const dominantColor = colors[dominantKey];
|
|
|
|
|
|
|
|
|
|
|
|
html += `<polygon points="${{points.join(' ')}}" fill="${{dominantColor}}" fill-opacity="0.4" stroke="${{dominantColor}}" stroke-width="2.5"/>`;
|
|
|
|
|
|
|
|
|
|
|
|
angles.forEach((angle, i) => {{
|
|
|
|
|
|
const normalizedScore = normalizedScores[keys[i]];
|
|
|
|
|
|
const rad = (angle - 90) * Math.PI / 180;
|
|
|
|
|
|
const r = normalizedScore * maxRadius;
|
|
|
|
|
|
const x = cx + r * Math.cos(rad);
|
|
|
|
|
|
const y = cy + r * Math.sin(rad);
|
|
|
|
|
|
html += `<circle cx="${{x}}" cy="${{y}}" r="4.5" fill="${{colors[keys[i]]}}" stroke="#0d1117" stroke-width="2"/>`;
|
|
|
|
|
|
}});
|
|
|
|
|
|
|
|
|
|
|
|
html += '</svg>';
|
|
|
|
|
|
html += '</div>';
|
|
|
|
|
|
return html;
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
2026-01-25 22:50:27 +01:00
|
|
|
|
// Tab functionality with hash-based routing
|
|
|
|
|
|
function switchTab(tabName) {{
|
|
|
|
|
|
// Hide all tabs
|
|
|
|
|
|
document.querySelectorAll('.tab-content').forEach(tab => {{
|
|
|
|
|
|
tab.classList.remove('active');
|
|
|
|
|
|
}});
|
|
|
|
|
|
|
|
|
|
|
|
// Remove active class from all buttons
|
|
|
|
|
|
document.querySelectorAll('.tab-button').forEach(btn => {{
|
|
|
|
|
|
btn.classList.remove('active');
|
|
|
|
|
|
}});
|
|
|
|
|
|
|
|
|
|
|
|
// Show selected tab
|
|
|
|
|
|
const selectedTab = document.getElementById(tabName);
|
|
|
|
|
|
const selectedButton = document.querySelector(`.tab-button[href="#${{tabName}}"]`);
|
|
|
|
|
|
|
|
|
|
|
|
if (selectedTab) {{
|
|
|
|
|
|
selectedTab.classList.add('active');
|
|
|
|
|
|
}}
|
|
|
|
|
|
if (selectedButton) {{
|
|
|
|
|
|
selectedButton.classList.add('active');
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
// Load data for this tab
|
|
|
|
|
|
if (tabName === 'ip-stats') {{
|
|
|
|
|
|
loadIpStatistics(1);
|
|
|
|
|
|
// Load attack and credentials tables if not already loaded
|
|
|
|
|
|
if (!overviewState.attacks.loaded) {{
|
|
|
|
|
|
loadOverviewTable('attacks');
|
|
|
|
|
|
overviewState.attacks.loaded = true;
|
|
|
|
|
|
}}
|
|
|
|
|
|
if (!overviewState.credentials.loaded) {{
|
|
|
|
|
|
loadOverviewTable('credentials');
|
|
|
|
|
|
overviewState.credentials.loaded = true;
|
|
|
|
|
|
}}
|
|
|
|
|
|
}}
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
// Handle hash changes
|
|
|
|
|
|
window.addEventListener('hashchange', function() {{
|
|
|
|
|
|
const hash = window.location.hash.slice(1) || 'overview';
|
|
|
|
|
|
switchTab(hash);
|
|
|
|
|
|
}});
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize tabs on page load
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {{
|
|
|
|
|
|
const hash = window.location.hash.slice(1) || 'overview';
|
|
|
|
|
|
switchTab(hash);
|
|
|
|
|
|
}});
|
|
|
|
|
|
|
|
|
|
|
|
// Prevent default anchor behavior and use hash navigation
|
|
|
|
|
|
document.querySelectorAll('.tab-button').forEach(button => {{
|
|
|
|
|
|
button.addEventListener('click', function(e) {{
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
const href = this.getAttribute('href');
|
|
|
|
|
|
window.location.hash = href;
|
|
|
|
|
|
}});
|
|
|
|
|
|
}});
|
|
|
|
|
|
|
|
|
|
|
|
// Handle sorting for IP stats table
|
|
|
|
|
|
document.addEventListener('click', function(e) {{
|
|
|
|
|
|
if (e.target.classList.contains('sortable') && e.target.closest('#ip-stats-tbody')) {{
|
|
|
|
|
|
return; // Don't sort when inside tbody
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
const sortHeader = e.target.closest('th.sortable');
|
|
|
|
|
|
if (!sortHeader) return;
|
|
|
|
|
|
|
|
|
|
|
|
const table = sortHeader.closest('table');
|
|
|
|
|
|
if (!table || !table.classList.contains('ip-stats-table')) return;
|
|
|
|
|
|
|
|
|
|
|
|
const sortField = sortHeader.getAttribute('data-sort');
|
|
|
|
|
|
|
|
|
|
|
|
// Toggle sort order if clicking the same field
|
|
|
|
|
|
if (currentSortBy === sortField) {{
|
|
|
|
|
|
currentSortOrder = currentSortOrder === 'desc' ? 'asc' : 'desc';
|
|
|
|
|
|
}} else {{
|
|
|
|
|
|
currentSortBy = sortField;
|
|
|
|
|
|
currentSortOrder = 'desc';
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
// Update UI indicators
|
|
|
|
|
|
table.querySelectorAll('th.sortable').forEach(th => {{
|
|
|
|
|
|
th.classList.remove('asc', 'desc');
|
|
|
|
|
|
}});
|
|
|
|
|
|
sortHeader.classList.add(currentSortOrder);
|
|
|
|
|
|
|
|
|
|
|
|
// Reload with new sort
|
|
|
|
|
|
loadIpStatistics(1);
|
|
|
|
|
|
}});
|
|
|
|
|
|
|
|
|
|
|
|
let currentPage = 1;
|
|
|
|
|
|
let totalPages = 1;
|
|
|
|
|
|
let currentSortBy = "total_requests";
|
|
|
|
|
|
let currentSortOrder = "desc";
|
|
|
|
|
|
const PAGE_SIZE = 5;
|
|
|
|
|
|
|
|
|
|
|
|
async function loadIpStatistics(page = 1) {{
|
|
|
|
|
|
const tbody = document.getElementById('ip-stats-tbody');
|
|
|
|
|
|
if (!tbody) {{
|
|
|
|
|
|
console.error('IP stats tbody not found');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
tbody.innerHTML = '<tr><td colspan="6" style="text-align: center;">Loading...</td></tr>';
|
|
|
|
|
|
|
|
|
|
|
|
try {{
|
|
|
|
|
|
console.log('Fetching attackers from page:', page, 'sort:', currentSortBy, currentSortOrder);
|
|
|
|
|
|
const response = await fetch(DASHBOARD_PATH + '/api/attackers?page=' + page + '&page_size=' + PAGE_SIZE + '&sort_by=' + currentSortBy + '&sort_order=' + currentSortOrder, {{
|
|
|
|
|
|
cache: 'no-store',
|
|
|
|
|
|
headers: {{
|
|
|
|
|
|
'Cache-Control': 'no-cache',
|
|
|
|
|
|
'Pragma': 'no-cache'
|
|
|
|
|
|
}}
|
|
|
|
|
|
}});
|
|
|
|
|
|
|
|
|
|
|
|
console.log('Response status:', response.status);
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) throw new Error(`HTTP ${{response.status}}`);
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
console.log('Received data:', data);
|
|
|
|
|
|
|
|
|
|
|
|
if (!data.attackers || data.attackers.length === 0) {{
|
|
|
|
|
|
tbody.innerHTML = '<tr><td colspan="6" style="text-align: center;">No attackers on this page.</td></tr>';
|
|
|
|
|
|
currentPage = page;
|
|
|
|
|
|
totalPages = data.pagination?.total_pages || 1;
|
|
|
|
|
|
updatePaginationControls();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
// Update pagination info
|
|
|
|
|
|
currentPage = data.pagination.page;
|
|
|
|
|
|
totalPages = data.pagination.total_pages;
|
|
|
|
|
|
document.getElementById('current-page').textContent = currentPage;
|
|
|
|
|
|
document.getElementById('total-pages').textContent = totalPages;
|
|
|
|
|
|
document.getElementById('total-attackers').textContent = data.pagination.total_attackers;
|
|
|
|
|
|
updatePaginationControls();
|
|
|
|
|
|
|
|
|
|
|
|
let html = '';
|
|
|
|
|
|
data.attackers.forEach((attacker, index) => {{
|
|
|
|
|
|
const rank = (currentPage - 1) * PAGE_SIZE + index + 1;
|
|
|
|
|
|
html += `<tr class="ip-row" data-ip="${{attacker.ip}}">
|
|
|
|
|
|
<td class="rank">${{rank}}</td>
|
|
|
|
|
|
<td class="ip-clickable">${{attacker.ip}}</td>
|
|
|
|
|
|
<td>${{attacker.total_requests}}</td>
|
|
|
|
|
|
<td>${{formatTimestamp(attacker.first_seen)}}</td>
|
|
|
|
|
|
<td>${{formatTimestamp(attacker.last_seen)}}</td>
|
2026-01-27 16:56:34 +01:00
|
|
|
|
<td>${{attacker.city ? (attacker.country_code ? `${{attacker.city}}, ${{attacker.country_code}}` : attacker.city) : (attacker.country_code || 'Unknown')}}</td>
|
2026-01-25 22:50:27 +01:00
|
|
|
|
</tr>
|
|
|
|
|
|
<tr class="ip-stats-row" id="stats-row-${{attacker.ip.replace('.', '-')}}" style="display: none;">
|
|
|
|
|
|
<td colspan="6" class="ip-stats-cell">
|
|
|
|
|
|
<div class="ip-stats-dropdown">
|
|
|
|
|
|
<div class="loading">Loading stats...</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>`;
|
|
|
|
|
|
}});
|
|
|
|
|
|
|
|
|
|
|
|
tbody.innerHTML = html;
|
|
|
|
|
|
console.log('Populated', data.attackers.length, 'attacker records');
|
|
|
|
|
|
|
|
|
|
|
|
// Re-attach click listeners for expandable rows
|
|
|
|
|
|
attachAttackerClickListeners();
|
|
|
|
|
|
}} catch (err) {{
|
|
|
|
|
|
console.error('Error loading attackers:', err);
|
|
|
|
|
|
tbody.innerHTML = `<tr><td colspan="6" style="text-align: center; color: #f85149;">Failed to load: ${{err.message}}</td></tr>`;
|
|
|
|
|
|
}}
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
function updatePaginationControls() {{
|
|
|
|
|
|
const prevBtn = document.getElementById('prev-page-btn');
|
|
|
|
|
|
const nextBtn = document.getElementById('next-page-btn');
|
|
|
|
|
|
|
|
|
|
|
|
if (prevBtn) prevBtn.disabled = currentPage <= 1;
|
|
|
|
|
|
if (nextBtn) nextBtn.disabled = currentPage >= totalPages;
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
function previousPageIpStats() {{
|
|
|
|
|
|
if (currentPage > 1) {{
|
|
|
|
|
|
loadIpStatistics(currentPage - 1);
|
|
|
|
|
|
}}
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
function nextPageIpStats() {{
|
|
|
|
|
|
if (currentPage < totalPages) {{
|
|
|
|
|
|
loadIpStatistics(currentPage + 1);
|
|
|
|
|
|
}}
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
function attachAttackerClickListeners() {{
|
|
|
|
|
|
document.querySelectorAll('#ip-stats-tbody .ip-clickable').forEach(cell => {{
|
|
|
|
|
|
cell.addEventListener('click', async function(e) {{
|
|
|
|
|
|
const row = e.currentTarget.closest('.ip-row');
|
|
|
|
|
|
if (!row) return;
|
|
|
|
|
|
|
|
|
|
|
|
const ip = row.getAttribute('data-ip');
|
|
|
|
|
|
const statsRow = row.nextElementSibling;
|
|
|
|
|
|
if (!statsRow || !statsRow.classList.contains('ip-stats-row')) return;
|
|
|
|
|
|
|
|
|
|
|
|
const isVisible = getComputedStyle(statsRow).display !== 'none';
|
|
|
|
|
|
|
|
|
|
|
|
// Close other open rows
|
|
|
|
|
|
document.querySelectorAll('#ip-stats-tbody .ip-stats-row').forEach(r => {{
|
|
|
|
|
|
r.style.display = 'none';
|
|
|
|
|
|
}});
|
|
|
|
|
|
|
|
|
|
|
|
if (isVisible) return;
|
|
|
|
|
|
|
|
|
|
|
|
statsRow.style.display = 'table-row';
|
|
|
|
|
|
|
|
|
|
|
|
const dropdown = statsRow.querySelector('.ip-stats-dropdown');
|
|
|
|
|
|
|
|
|
|
|
|
if (dropdown) {{
|
|
|
|
|
|
dropdown.innerHTML = '<div class="loading">Loading stats...</div>';
|
|
|
|
|
|
try {{
|
|
|
|
|
|
const response = await fetch(`${{DASHBOARD_PATH}}/api/ip-stats/${{ip}}`, {{
|
|
|
|
|
|
cache: 'no-store',
|
|
|
|
|
|
headers: {{
|
|
|
|
|
|
'Cache-Control': 'no-cache',
|
|
|
|
|
|
'Pragma': 'no-cache'
|
|
|
|
|
|
}}
|
|
|
|
|
|
}});
|
|
|
|
|
|
if (!response.ok) throw new Error(`HTTP ${{response.status}}`);
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
dropdown.innerHTML = data.error
|
|
|
|
|
|
? `<div style="color:#f85149;">Error: ${{data.error}}</div>`
|
|
|
|
|
|
: formatIpStats(data);
|
|
|
|
|
|
}} catch (err) {{
|
|
|
|
|
|
dropdown.innerHTML = `<div style="color:#f85149;">Failed to load stats: ${{err.message}}</div>`;
|
|
|
|
|
|
}}
|
|
|
|
|
|
}}
|
|
|
|
|
|
}});
|
|
|
|
|
|
}});
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
function attachTopIpsClickListeners() {{
|
|
|
|
|
|
document.querySelectorAll('#top-ips-tbody .ip-clickable').forEach(cell => {{
|
|
|
|
|
|
cell.addEventListener('click', async function(e) {{
|
|
|
|
|
|
const row = e.currentTarget.closest('.ip-row');
|
|
|
|
|
|
if (!row) return;
|
|
|
|
|
|
|
|
|
|
|
|
const ip = row.getAttribute('data-ip');
|
|
|
|
|
|
const statsRow = row.nextElementSibling;
|
|
|
|
|
|
if (!statsRow || !statsRow.classList.contains('ip-stats-row')) return;
|
|
|
|
|
|
|
|
|
|
|
|
const isVisible = getComputedStyle(statsRow).display !== 'none';
|
|
|
|
|
|
|
|
|
|
|
|
// Close other open rows in this table
|
|
|
|
|
|
document.querySelectorAll('#top-ips-tbody .ip-stats-row').forEach(r => {{
|
|
|
|
|
|
r.style.display = 'none';
|
|
|
|
|
|
}});
|
|
|
|
|
|
|
|
|
|
|
|
if (isVisible) return;
|
|
|
|
|
|
|
|
|
|
|
|
statsRow.style.display = 'table-row';
|
|
|
|
|
|
|
|
|
|
|
|
const dropdown = statsRow.querySelector('.ip-stats-dropdown');
|
|
|
|
|
|
|
|
|
|
|
|
if (dropdown) {{
|
|
|
|
|
|
dropdown.innerHTML = '<div class="loading">Loading stats...</div>';
|
|
|
|
|
|
try {{
|
|
|
|
|
|
const response = await fetch(`${{DASHBOARD_PATH}}/api/ip-stats/${{ip}}`, {{
|
|
|
|
|
|
cache: 'no-store',
|
|
|
|
|
|
headers: {{
|
|
|
|
|
|
'Cache-Control': 'no-cache',
|
|
|
|
|
|
'Pragma': 'no-cache'
|
|
|
|
|
|
}}
|
|
|
|
|
|
}});
|
|
|
|
|
|
if (!response.ok) throw new Error(`HTTP ${{response.status}}`);
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
dropdown.innerHTML = data.error
|
|
|
|
|
|
? `<div style="color:#f85149;">Error: ${{data.error}}</div>`
|
|
|
|
|
|
: formatIpStats(data);
|
|
|
|
|
|
}} catch (err) {{
|
|
|
|
|
|
dropdown.innerHTML = `<div style="color:#f85149;">Failed to load stats: ${{err.message}}</div>`;
|
|
|
|
|
|
}}
|
|
|
|
|
|
}}
|
|
|
|
|
|
}});
|
|
|
|
|
|
}});
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
function attachHoneypotClickListeners() {{
|
|
|
|
|
|
document.querySelectorAll('#honeypot-tbody .ip-clickable').forEach(cell => {{
|
|
|
|
|
|
cell.addEventListener('click', async function(e) {{
|
|
|
|
|
|
const row = e.currentTarget.closest('.ip-row');
|
|
|
|
|
|
if (!row) return;
|
|
|
|
|
|
|
|
|
|
|
|
const ip = row.getAttribute('data-ip');
|
|
|
|
|
|
const statsRow = row.nextElementSibling;
|
|
|
|
|
|
if (!statsRow || !statsRow.classList.contains('ip-stats-row')) return;
|
|
|
|
|
|
|
|
|
|
|
|
const isVisible = getComputedStyle(statsRow).display !== 'none';
|
|
|
|
|
|
|
|
|
|
|
|
document.querySelectorAll('#honeypot-tbody .ip-stats-row').forEach(r => {{
|
|
|
|
|
|
r.style.display = 'none';
|
|
|
|
|
|
}});
|
|
|
|
|
|
|
|
|
|
|
|
if (isVisible) return;
|
|
|
|
|
|
|
|
|
|
|
|
statsRow.style.display = 'table-row';
|
|
|
|
|
|
|
|
|
|
|
|
const dropdown = statsRow.querySelector('.ip-stats-dropdown');
|
|
|
|
|
|
|
|
|
|
|
|
if (dropdown) {{
|
|
|
|
|
|
dropdown.innerHTML = '<div class="loading">Loading stats...</div>';
|
|
|
|
|
|
try {{
|
|
|
|
|
|
const response = await fetch(`${{DASHBOARD_PATH}}/api/ip-stats/${{ip}}`, {{
|
|
|
|
|
|
cache: 'no-store',
|
|
|
|
|
|
headers: {{
|
|
|
|
|
|
'Cache-Control': 'no-cache',
|
|
|
|
|
|
'Pragma': 'no-cache'
|
|
|
|
|
|
}}
|
|
|
|
|
|
}});
|
|
|
|
|
|
if (!response.ok) throw new Error(`HTTP ${{response.status}}`);
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
dropdown.innerHTML = data.error
|
|
|
|
|
|
? `<div style="color:#f85149;">Error: ${{data.error}}</div>`
|
|
|
|
|
|
: formatIpStats(data);
|
|
|
|
|
|
}} catch (err) {{
|
|
|
|
|
|
dropdown.innerHTML = `<div style="color:#f85149;">Failed to load stats: ${{err.message}}</div>`;
|
|
|
|
|
|
}}
|
|
|
|
|
|
}}
|
|
|
|
|
|
}});
|
|
|
|
|
|
}});
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
function attachCredentialsClickListeners() {{
|
|
|
|
|
|
document.querySelectorAll('#credentials-tbody .ip-clickable').forEach(cell => {{
|
|
|
|
|
|
cell.addEventListener('click', async function(e) {{
|
|
|
|
|
|
const row = e.currentTarget.closest('.ip-row');
|
|
|
|
|
|
if (!row) return;
|
|
|
|
|
|
|
|
|
|
|
|
const ip = row.getAttribute('data-ip');
|
|
|
|
|
|
const statsRow = row.nextElementSibling;
|
|
|
|
|
|
if (!statsRow || !statsRow.classList.contains('ip-stats-row')) return;
|
|
|
|
|
|
|
|
|
|
|
|
const isVisible = getComputedStyle(statsRow).display !== 'none';
|
|
|
|
|
|
|
|
|
|
|
|
document.querySelectorAll('#credentials-tbody .ip-stats-row').forEach(r => {{
|
|
|
|
|
|
r.style.display = 'none';
|
|
|
|
|
|
}});
|
|
|
|
|
|
|
|
|
|
|
|
if (isVisible) return;
|
|
|
|
|
|
|
|
|
|
|
|
statsRow.style.display = 'table-row';
|
|
|
|
|
|
|
|
|
|
|
|
const dropdown = statsRow.querySelector('.ip-stats-dropdown');
|
|
|
|
|
|
|
|
|
|
|
|
if (dropdown) {{
|
|
|
|
|
|
dropdown.innerHTML = '<div class="loading">Loading stats...</div>';
|
|
|
|
|
|
try {{
|
|
|
|
|
|
const response = await fetch(`${{DASHBOARD_PATH}}/api/ip-stats/${{ip}}`, {{
|
|
|
|
|
|
cache: 'no-store',
|
|
|
|
|
|
headers: {{
|
|
|
|
|
|
'Cache-Control': 'no-cache',
|
|
|
|
|
|
'Pragma': 'no-cache'
|
|
|
|
|
|
}}
|
|
|
|
|
|
}});
|
|
|
|
|
|
if (!response.ok) throw new Error(`HTTP ${{response.status}}`);
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
dropdown.innerHTML = data.error
|
|
|
|
|
|
? `<div style="color:#f85149;">Error: ${{data.error}}</div>`
|
|
|
|
|
|
: formatIpStats(data);
|
|
|
|
|
|
}} catch (err) {{
|
|
|
|
|
|
dropdown.innerHTML = `<div style="color:#f85149;">Failed to load stats: ${{err.message}}</div>`;
|
|
|
|
|
|
}}
|
|
|
|
|
|
}}
|
|
|
|
|
|
}});
|
|
|
|
|
|
}});
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
function attachAttacksClickListeners() {{
|
|
|
|
|
|
document.querySelectorAll('#attacks-tbody .ip-clickable').forEach(cell => {{
|
|
|
|
|
|
cell.addEventListener('click', async function(e) {{
|
|
|
|
|
|
const row = e.currentTarget.closest('.ip-row');
|
|
|
|
|
|
if (!row) return;
|
|
|
|
|
|
|
|
|
|
|
|
const ip = row.getAttribute('data-ip');
|
|
|
|
|
|
const statsRow = row.nextElementSibling;
|
|
|
|
|
|
if (!statsRow || !statsRow.classList.contains('ip-stats-row')) return;
|
|
|
|
|
|
|
|
|
|
|
|
const isVisible = getComputedStyle(statsRow).display !== 'none';
|
|
|
|
|
|
|
|
|
|
|
|
document.querySelectorAll('#attacks-tbody .ip-stats-row').forEach(r => {{
|
|
|
|
|
|
r.style.display = 'none';
|
|
|
|
|
|
}});
|
|
|
|
|
|
|
|
|
|
|
|
if (isVisible) return;
|
|
|
|
|
|
|
|
|
|
|
|
statsRow.style.display = 'table-row';
|
|
|
|
|
|
|
|
|
|
|
|
const dropdown = statsRow.querySelector('.ip-stats-dropdown');
|
|
|
|
|
|
|
|
|
|
|
|
if (dropdown) {{
|
|
|
|
|
|
dropdown.innerHTML = '<div class="loading">Loading stats...</div>';
|
|
|
|
|
|
try {{
|
|
|
|
|
|
const response = await fetch(`${{DASHBOARD_PATH}}/api/ip-stats/${{ip}}`, {{
|
|
|
|
|
|
cache: 'no-store',
|
|
|
|
|
|
headers: {{
|
|
|
|
|
|
'Cache-Control': 'no-cache',
|
|
|
|
|
|
'Pragma': 'no-cache'
|
|
|
|
|
|
}}
|
|
|
|
|
|
}});
|
|
|
|
|
|
if (!response.ok) throw new Error(`HTTP ${{response.status}}`);
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
dropdown.innerHTML = data.error
|
|
|
|
|
|
? `<div style="color:#f85149;">Error: ${{data.error}}</div>`
|
|
|
|
|
|
: formatIpStats(data);
|
|
|
|
|
|
}} catch (err) {{
|
|
|
|
|
|
dropdown.innerHTML = `<div style="color:#f85149;">Failed to load stats: ${{err.message}}</div>`;
|
|
|
|
|
|
}}
|
|
|
|
|
|
}}
|
|
|
|
|
|
}});
|
|
|
|
|
|
}});
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
// Overview tables state management
|
|
|
|
|
|
const overviewState = {{
|
|
|
|
|
|
honeypot: {{ currentPage: 1, totalPages: 1, total: 0, sortBy: 'count', sortOrder: 'desc' }},
|
|
|
|
|
|
credentials: {{ currentPage: 1, totalPages: 1, total: 0, sortBy: 'timestamp', sortOrder: 'desc', loaded: false }},
|
|
|
|
|
|
'top-ips': {{ currentPage: 1, totalPages: 1, total: 0, sortBy: 'count', sortOrder: 'desc' }},
|
|
|
|
|
|
'top-paths': {{ currentPage: 1, totalPages: 1, total: 0, sortBy: 'count', sortOrder: 'desc' }},
|
|
|
|
|
|
'top-ua': {{ currentPage: 1, totalPages: 1, total: 0, sortBy: 'count', sortOrder: 'desc' }},
|
|
|
|
|
|
attacks: {{ currentPage: 1, totalPages: 1, total: 0, sortBy: 'timestamp', sortOrder: 'desc', loaded: false }}
|
|
|
|
|
|
}};
|
|
|
|
|
|
|
|
|
|
|
|
const tableConfig = {{
|
|
|
|
|
|
honeypot: {{ endpoint: 'honeypot', dataKey: 'honeypots', cellCount: 4, columns: ['ip', 'paths', 'count'] }},
|
|
|
|
|
|
credentials: {{ endpoint: 'credentials', dataKey: 'credentials', cellCount: 6, columns: ['ip', 'username', 'password', 'path', 'timestamp'] }},
|
|
|
|
|
|
'top-ips': {{ endpoint: 'top-ips', dataKey: 'ips', cellCount: 3, columns: ['ip', 'count'] }},
|
|
|
|
|
|
'top-paths': {{ endpoint: 'top-paths', dataKey: 'paths', cellCount: 3, columns: ['path', 'count'] }},
|
|
|
|
|
|
'top-ua': {{ endpoint: 'top-user-agents', dataKey: 'user_agents', cellCount: 3, columns: ['user_agent', 'count'] }},
|
|
|
|
|
|
attacks: {{ endpoint: 'attack-types', dataKey: 'attacks', cellCount: 6, columns: ['ip', 'path', 'attack_types', 'user_agent', 'timestamp'] }}
|
|
|
|
|
|
}};
|
|
|
|
|
|
|
|
|
|
|
|
// Load overview table on page load
|
|
|
|
|
|
async function loadOverviewTable(tableId) {{
|
|
|
|
|
|
const config = tableConfig[tableId];
|
|
|
|
|
|
if (!config) return;
|
|
|
|
|
|
|
|
|
|
|
|
const state = overviewState[tableId];
|
|
|
|
|
|
const tbody = document.getElementById(tableId + '-tbody');
|
|
|
|
|
|
if (!tbody) return;
|
|
|
|
|
|
|
|
|
|
|
|
// Just fade out without showing loading text
|
|
|
|
|
|
tbody.style.opacity = '0';
|
|
|
|
|
|
|
|
|
|
|
|
try {{
|
|
|
|
|
|
const url = DASHBOARD_PATH + '/api/' + config.endpoint + '?page=' + state.currentPage + '&page_size=5&sort_by=' + state.sortBy + '&sort_order=' + state.sortOrder;
|
|
|
|
|
|
const response = await fetch(url, {{ cache: 'no-store', headers: {{ 'Cache-Control': 'no-cache', 'Pragma': 'no-cache' }} }});
|
|
|
|
|
|
if (!response.ok) throw new Error(`HTTP ${{response.status}}`);
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
const items = data[config.dataKey] || [];
|
|
|
|
|
|
const pagination = data.pagination || {{}};
|
|
|
|
|
|
|
|
|
|
|
|
state.currentPage = pagination.page || 1;
|
|
|
|
|
|
state.totalPages = pagination.total_pages || 1;
|
|
|
|
|
|
state.total = pagination.total || 0;
|
|
|
|
|
|
updateOverviewPaginationControls(tableId);
|
|
|
|
|
|
|
|
|
|
|
|
if (items.length === 0) {{
|
|
|
|
|
|
tbody.style.opacity = '0';
|
|
|
|
|
|
setTimeout(() => {{
|
|
|
|
|
|
tbody.innerHTML = '<tr><td colspan="' + config.cellCount + '" style="text-align: center; color: #6e7681; padding: 20px; font-size: 13px;">No data</td></tr>';
|
|
|
|
|
|
tbody.style.opacity = '1';
|
|
|
|
|
|
}}, 50);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
let html = '';
|
|
|
|
|
|
items.forEach((item, index) => {{
|
|
|
|
|
|
const rank = (state.currentPage - 1) * 5 + index + 1;
|
|
|
|
|
|
|
|
|
|
|
|
if (tableId === 'honeypot') {{
|
|
|
|
|
|
html += `<tr class="ip-row" data-ip="${{item.ip}}"><td class="rank">${{rank}}</td><td class="ip-clickable">${{item.ip}}</td><td>${{item.paths.join(', ')}}</td><td>${{item.count}}</td></tr>`;
|
|
|
|
|
|
html += `<tr class="ip-stats-row" id="stats-row-honeypot-${{item.ip.replace(/\\./g, '-')}}" style="display: none;">
|
|
|
|
|
|
<td colspan="4" class="ip-stats-cell">
|
|
|
|
|
|
<div class="ip-stats-dropdown">
|
|
|
|
|
|
<div class="loading">Loading stats...</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>`;
|
|
|
|
|
|
}} else if (tableId === 'credentials') {{
|
|
|
|
|
|
html += `<tr class="ip-row" data-ip="${{item.ip}}"><td class="rank">${{rank}}</td><td class="ip-clickable">${{item.ip}}</td><td>${{item.username}}</td><td>${{item.password}}</td><td>${{item.path}}</td><td>${{formatTimestamp(item.timestamp, true)}}</td></tr>`;
|
|
|
|
|
|
html += `<tr class="ip-stats-row" id="stats-row-credentials-${{item.ip.replace(/\\./g, '-')}}" style="display: none;">
|
|
|
|
|
|
<td colspan="6" class="ip-stats-cell">
|
|
|
|
|
|
<div class="ip-stats-dropdown">
|
|
|
|
|
|
<div class="loading">Loading stats...</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>`;
|
|
|
|
|
|
}} else if (tableId === 'top-ips') {{
|
|
|
|
|
|
html += `<tr class="ip-row" data-ip="${{item.ip}}"><td class="rank">${{rank}}</td><td class="ip-clickable">${{item.ip}}</td><td>${{item.count}}</td></tr>`;
|
|
|
|
|
|
html += `<tr class="ip-stats-row" id="stats-row-top-ips-${{item.ip.replace(/\\./g, '-')}}" style="display: none;">
|
|
|
|
|
|
<td colspan="3" class="ip-stats-cell">
|
|
|
|
|
|
<div class="ip-stats-dropdown">
|
|
|
|
|
|
<div class="loading">Loading stats...</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>`;
|
|
|
|
|
|
}} else if (tableId === 'top-paths') {{
|
|
|
|
|
|
html += `<tr><td class="rank">${{rank}}</td><td>${{item.path}}</td><td>${{item.count}}</td></tr>`;
|
|
|
|
|
|
}} else if (tableId === 'top-ua') {{
|
|
|
|
|
|
html += `<tr><td class="rank">${{rank}}</td><td style="word-break: break-all;">${{item.user_agent.substring(0, 80)}}</td><td>${{item.count}}</td></tr>`;
|
|
|
|
|
|
}} else if (tableId === 'attacks') {{
|
|
|
|
|
|
html += `<tr class="ip-row" data-ip="${{item.ip}}"><td class="rank">${{rank}}</td><td class="ip-clickable">${{item.ip}}</td><td>${{item.path}}</td><td>${{item.attack_types.join(', ')}}</td><td style="word-break: break-all;">${{item.user_agent.substring(0, 60)}}</td><td>${{formatTimestamp(item.timestamp, true)}}</td></tr>`;
|
|
|
|
|
|
html += `<tr class="ip-stats-row" id="stats-row-attacks-${{item.ip.replace(/\\./g, '-')}}" style="display: none;">
|
|
|
|
|
|
<td colspan="6" class="ip-stats-cell">
|
|
|
|
|
|
<div class="ip-stats-dropdown">
|
|
|
|
|
|
<div class="loading">Loading stats...</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>`;
|
|
|
|
|
|
}}
|
|
|
|
|
|
}});
|
|
|
|
|
|
|
|
|
|
|
|
// Fade in new content
|
|
|
|
|
|
tbody.style.opacity = '0';
|
|
|
|
|
|
setTimeout(() => {{
|
|
|
|
|
|
tbody.innerHTML = html;
|
|
|
|
|
|
tbody.style.opacity = '1';
|
|
|
|
|
|
|
|
|
|
|
|
// Attach click listeners for IP cells in tables
|
|
|
|
|
|
if (tableId === 'top-ips') {{
|
|
|
|
|
|
attachTopIpsClickListeners();
|
|
|
|
|
|
}} else if (tableId === 'honeypot') {{
|
|
|
|
|
|
attachHoneypotClickListeners();
|
|
|
|
|
|
}} else if (tableId === 'credentials') {{
|
|
|
|
|
|
attachCredentialsClickListeners();
|
|
|
|
|
|
}} else if (tableId === 'attacks') {{
|
|
|
|
|
|
attachAttacksClickListeners();
|
|
|
|
|
|
}}
|
|
|
|
|
|
}}, 50);
|
|
|
|
|
|
}} catch (err) {{
|
|
|
|
|
|
console.error('Error loading overview table ' + tableId + ':', err);
|
|
|
|
|
|
tbody.style.opacity = '0';
|
|
|
|
|
|
setTimeout(() => {{
|
|
|
|
|
|
tbody.innerHTML = '<tr><td colspan="' + config.cellCount + '" style="text-align: center; color: #f85149; padding: 20px; font-size: 13px;">Failed to load</td></tr>';
|
|
|
|
|
|
tbody.style.opacity = '1';
|
|
|
|
|
|
}}, 50);
|
|
|
|
|
|
}}
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
function updateOverviewPaginationControls(tableId) {{
|
|
|
|
|
|
const state = overviewState[tableId];
|
|
|
|
|
|
const pagination = document.getElementById(tableId + '-pagination');
|
|
|
|
|
|
if (!pagination) return;
|
|
|
|
|
|
|
|
|
|
|
|
const prevBtn = pagination.querySelector('.pagination-btn:nth-child(2)');
|
|
|
|
|
|
const nextBtn = pagination.querySelector('.pagination-btn:nth-child(3)');
|
|
|
|
|
|
const currentPageEl = pagination.querySelector('.current-page');
|
|
|
|
|
|
const totalPagesEl = pagination.querySelector('.total-pages');
|
|
|
|
|
|
const totalRecordsEl = pagination.querySelector('.total-records');
|
|
|
|
|
|
|
|
|
|
|
|
if (prevBtn) prevBtn.disabled = state.currentPage <= 1;
|
|
|
|
|
|
if (nextBtn) nextBtn.disabled = state.currentPage >= state.totalPages;
|
|
|
|
|
|
if (currentPageEl) currentPageEl.textContent = state.currentPage;
|
|
|
|
|
|
if (totalPagesEl) totalPagesEl.textContent = state.totalPages;
|
|
|
|
|
|
if (totalRecordsEl) totalRecordsEl.textContent = state.total;
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
function previousPage(tableId) {{
|
|
|
|
|
|
if (overviewState[tableId].currentPage > 1) {{
|
|
|
|
|
|
overviewState[tableId].currentPage--;
|
|
|
|
|
|
loadOverviewTable(tableId);
|
|
|
|
|
|
}}
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
function nextPage(tableId) {{
|
|
|
|
|
|
if (overviewState[tableId].currentPage < overviewState[tableId].totalPages) {{
|
|
|
|
|
|
overviewState[tableId].currentPage++;
|
|
|
|
|
|
loadOverviewTable(tableId);
|
|
|
|
|
|
}}
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
// Handle sorting for overview tables
|
|
|
|
|
|
document.addEventListener('click', function(e) {{
|
|
|
|
|
|
const header = e.target.closest('th.sortable[data-table]');
|
|
|
|
|
|
if (!header) return;
|
|
|
|
|
|
|
|
|
|
|
|
const tableId = header.getAttribute('data-table');
|
|
|
|
|
|
const sortField = header.getAttribute('data-sort');
|
|
|
|
|
|
const state = overviewState[tableId];
|
|
|
|
|
|
if (!state) return;
|
|
|
|
|
|
|
|
|
|
|
|
// Toggle sort order if same field
|
|
|
|
|
|
if (state.sortBy === sortField) {{
|
|
|
|
|
|
state.sortOrder = state.sortOrder === 'desc' ? 'asc' : 'desc';
|
|
|
|
|
|
}} else {{
|
|
|
|
|
|
state.sortBy = sortField;
|
|
|
|
|
|
state.sortOrder = 'desc';
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
// Update UI and reload
|
|
|
|
|
|
const table = header.closest('table');
|
|
|
|
|
|
if (table) {{
|
|
|
|
|
|
table.querySelectorAll('th.sortable').forEach(th => {{
|
|
|
|
|
|
th.classList.remove('asc', 'desc');
|
|
|
|
|
|
}});
|
|
|
|
|
|
header.classList.add(state.sortOrder);
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
state.currentPage = 1;
|
|
|
|
|
|
loadOverviewTable(tableId);
|
|
|
|
|
|
}});
|
|
|
|
|
|
|
|
|
|
|
|
// Load all overview tables when page loads
|
|
|
|
|
|
window.addEventListener('load', function() {{
|
|
|
|
|
|
// Only load tables that are in the Overview tab
|
|
|
|
|
|
const overviewTableIds = ['honeypot', 'top-ips', 'top-paths', 'top-ua'];
|
|
|
|
|
|
overviewTableIds.forEach(tableId => {{
|
|
|
|
|
|
loadOverviewTable(tableId);
|
|
|
|
|
|
}});
|
|
|
|
|
|
}})
|
|
|
|
|
|
|
|
|
|
|
|
async function showIpDetail(ip) {{
|
|
|
|
|
|
const modal = document.getElementById('ip-detail-modal');
|
|
|
|
|
|
const bodyDiv = document.getElementById('ip-detail-body');
|
|
|
|
|
|
|
|
|
|
|
|
if (!modal || !bodyDiv) return;
|
|
|
|
|
|
|
|
|
|
|
|
bodyDiv.innerHTML = '<div class="loading" style="text-align: center;">Loading IP details...</div>';
|
|
|
|
|
|
modal.classList.add('show');
|
|
|
|
|
|
|
|
|
|
|
|
try {{
|
|
|
|
|
|
const response = await fetch(`${{DASHBOARD_PATH}}/api/ip-stats/${{ip}}`, {{
|
|
|
|
|
|
cache: 'no-store',
|
|
|
|
|
|
headers: {{
|
|
|
|
|
|
'Cache-Control': 'no-cache',
|
|
|
|
|
|
'Pragma': 'no-cache'
|
|
|
|
|
|
}}
|
|
|
|
|
|
}});
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) throw new Error(`HTTP ${{response.status}}`);
|
|
|
|
|
|
|
|
|
|
|
|
const stats = await response.json();
|
|
|
|
|
|
bodyDiv.innerHTML = '<h2>' + stats.ip + ' - Detailed Statistics</h2>' + formatIpStats(stats);
|
|
|
|
|
|
}} catch (err) {{
|
|
|
|
|
|
bodyDiv.innerHTML = `<div style="color: #f85149;">Failed to load details: ${{err.message}}</div>`;
|
|
|
|
|
|
}}
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
function closeIpDetailModal() {{
|
|
|
|
|
|
const modal = document.getElementById('ip-detail-modal');
|
|
|
|
|
|
if (modal) {{
|
|
|
|
|
|
modal.classList.remove('show');
|
|
|
|
|
|
}}
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
// Close modal when clicking outside
|
|
|
|
|
|
document.getElementById('ip-detail-modal')?.addEventListener('click', function(e) {{
|
|
|
|
|
|
if (e.target === this) {{
|
|
|
|
|
|
closeIpDetailModal();
|
|
|
|
|
|
}}
|
|
|
|
|
|
}});
|
|
|
|
|
|
|
|
|
|
|
|
// Add CSS for view button
|
|
|
|
|
|
const style = document.createElement('style');
|
|
|
|
|
|
style.textContent = `
|
|
|
|
|
|
.view-btn {{
|
|
|
|
|
|
padding: 6px 12px;
|
|
|
|
|
|
background: #238636;
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
border: 1px solid #2ea043;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
transition: background 0.2s;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.view-btn:hover {{
|
|
|
|
|
|
background: #2ea043;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.view-btn:active {{
|
|
|
|
|
|
background: #1f7a2f;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.pagination-btn:hover:not(:disabled) {{
|
|
|
|
|
|
background: #1f6feb !important;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.pagination-btn:disabled {{
|
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
|
}}
|
|
|
|
|
|
`;
|
|
|
|
|
|
document.head.appendChild(style);
|
|
|
|
|
|
|
2026-01-26 12:36:22 +01:00
|
|
|
|
// IP Map Visualization
|
2026-01-25 22:50:27 +01:00
|
|
|
|
let attackerMap = null;
|
2026-01-26 12:36:22 +01:00
|
|
|
|
let allIps = [];
|
2026-01-25 22:50:27 +01:00
|
|
|
|
let mapMarkers = [];
|
2026-01-26 12:36:22 +01:00
|
|
|
|
let markerLayers = {{}};
|
|
|
|
|
|
|
|
|
|
|
|
const categoryColors = {{
|
|
|
|
|
|
attacker: '#f85149',
|
|
|
|
|
|
bad_crawler: '#f0883e',
|
|
|
|
|
|
good_crawler: '#3fb950',
|
|
|
|
|
|
regular_user: '#58a6ff',
|
|
|
|
|
|
unknown: '#8b949e'
|
|
|
|
|
|
}};
|
2026-01-25 22:50:27 +01:00
|
|
|
|
|
|
|
|
|
|
async function initializeAttackerMap() {{
|
|
|
|
|
|
const mapContainer = document.getElementById('attacker-map');
|
|
|
|
|
|
if (!mapContainer || attackerMap) return;
|
|
|
|
|
|
|
|
|
|
|
|
try {{
|
|
|
|
|
|
// Initialize map
|
|
|
|
|
|
attackerMap = L.map('attacker-map', {{
|
|
|
|
|
|
center: [20, 0],
|
|
|
|
|
|
zoom: 2,
|
|
|
|
|
|
layers: [
|
|
|
|
|
|
L.tileLayer('https://{{s}}.basemaps.cartocdn.com/dark_all/{{z}}/{{x}}/{{y}}{{r}}.png', {{
|
|
|
|
|
|
attribution: '© CartoDB | © OpenStreetMap contributors',
|
|
|
|
|
|
maxZoom: 19,
|
|
|
|
|
|
subdomains: 'abcd'
|
|
|
|
|
|
}})
|
|
|
|
|
|
]
|
|
|
|
|
|
}});
|
|
|
|
|
|
|
2026-01-26 12:36:22 +01:00
|
|
|
|
// Fetch all IPs (not just attackers)
|
|
|
|
|
|
const response = await fetch(DASHBOARD_PATH + '/api/all-ips?page=1&page_size=100&sort_by=total_requests&sort_order=desc', {{
|
2026-01-25 22:50:27 +01:00
|
|
|
|
cache: 'no-store',
|
|
|
|
|
|
headers: {{
|
|
|
|
|
|
'Cache-Control': 'no-cache',
|
|
|
|
|
|
'Pragma': 'no-cache'
|
|
|
|
|
|
}}
|
|
|
|
|
|
}});
|
|
|
|
|
|
|
2026-01-26 12:36:22 +01:00
|
|
|
|
if (!response.ok) throw new Error('Failed to fetch IPs');
|
|
|
|
|
|
|
2026-01-25 22:50:27 +01:00
|
|
|
|
const data = await response.json();
|
2026-01-26 12:36:22 +01:00
|
|
|
|
allIps = data.ips || [];
|
2026-01-25 22:50:27 +01:00
|
|
|
|
|
2026-01-26 12:36:22 +01:00
|
|
|
|
if (allIps.length === 0) {{
|
|
|
|
|
|
mapContainer.innerHTML = '<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #8b949e;\">No IP location data available</div>';
|
2026-01-25 22:50:27 +01:00
|
|
|
|
return;
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
// Get max request count for scaling
|
2026-01-26 12:36:22 +01:00
|
|
|
|
const maxRequests = Math.max(...allIps.map(ip => ip.total_requests || 0));
|
2026-01-25 22:50:27 +01:00
|
|
|
|
|
2026-01-27 16:56:34 +01:00
|
|
|
|
// City coordinates database (major cities worldwide)
|
|
|
|
|
|
const cityCoordinates = {{
|
|
|
|
|
|
// United States
|
|
|
|
|
|
'New York': [40.7128, -74.0060], 'Los Angeles': [34.0522, -118.2437],
|
|
|
|
|
|
'San Francisco': [37.7749, -122.4194], 'Chicago': [41.8781, -87.6298],
|
|
|
|
|
|
'Seattle': [47.6062, -122.3321], 'Miami': [25.7617, -80.1918],
|
|
|
|
|
|
'Boston': [42.3601, -71.0589], 'Atlanta': [33.7490, -84.3880],
|
|
|
|
|
|
'Dallas': [32.7767, -96.7970], 'Houston': [29.7604, -95.3698],
|
|
|
|
|
|
'Denver': [39.7392, -104.9903], 'Phoenix': [33.4484, -112.0740],
|
|
|
|
|
|
// Europe
|
|
|
|
|
|
'London': [51.5074, -0.1278], 'Paris': [48.8566, 2.3522],
|
|
|
|
|
|
'Berlin': [52.5200, 13.4050], 'Amsterdam': [52.3676, 4.9041],
|
|
|
|
|
|
'Moscow': [55.7558, 37.6173], 'Rome': [41.9028, 12.4964],
|
|
|
|
|
|
'Madrid': [40.4168, -3.7038], 'Barcelona': [41.3874, 2.1686],
|
|
|
|
|
|
'Milan': [45.4642, 9.1900], 'Vienna': [48.2082, 16.3738],
|
|
|
|
|
|
'Stockholm': [59.3293, 18.0686], 'Oslo': [59.9139, 10.7522],
|
|
|
|
|
|
'Copenhagen': [55.6761, 12.5683], 'Warsaw': [52.2297, 21.0122],
|
|
|
|
|
|
'Prague': [50.0755, 14.4378], 'Budapest': [47.4979, 19.0402],
|
|
|
|
|
|
'Athens': [37.9838, 23.7275], 'Lisbon': [38.7223, -9.1393],
|
|
|
|
|
|
'Brussels': [50.8503, 4.3517], 'Dublin': [53.3498, -6.2603],
|
|
|
|
|
|
'Zurich': [47.3769, 8.5417], 'Geneva': [46.2044, 6.1432],
|
|
|
|
|
|
'Helsinki': [60.1699, 24.9384], 'Bucharest': [44.4268, 26.1025],
|
|
|
|
|
|
'Saint Petersburg': [59.9343, 30.3351], 'Manchester': [53.4808, -2.2426],
|
|
|
|
|
|
'Roubaix': [50.6942, 3.1746], 'Frankfurt': [50.1109, 8.6821],
|
|
|
|
|
|
'Munich': [48.1351, 11.5820], 'Hamburg': [53.5511, 9.9937],
|
|
|
|
|
|
// Asia
|
|
|
|
|
|
'Tokyo': [35.6762, 139.6503], 'Beijing': [39.9042, 116.4074],
|
|
|
|
|
|
'Shanghai': [31.2304, 121.4737], 'Singapore': [1.3521, 103.8198],
|
|
|
|
|
|
'Mumbai': [19.0760, 72.8777], 'Delhi': [28.7041, 77.1025],
|
|
|
|
|
|
'Bangalore': [12.9716, 77.5946], 'Seoul': [37.5665, 126.9780],
|
|
|
|
|
|
'Hong Kong': [22.3193, 114.1694], 'Bangkok': [13.7563, 100.5018],
|
|
|
|
|
|
'Jakarta': [6.2088, 106.8456], 'Manila': [14.5995, 120.9842],
|
|
|
|
|
|
'Hanoi': [21.0285, 105.8542], 'Ho Chi Minh City': [10.8231, 106.6297],
|
|
|
|
|
|
'Taipei': [25.0330, 121.5654], 'Kuala Lumpur': [3.1390, 101.6869],
|
|
|
|
|
|
'Karachi': [24.8607, 67.0011], 'Islamabad': [33.6844, 73.0479],
|
|
|
|
|
|
'Dhaka': [23.8103, 90.4125], 'Colombo': [6.9271, 79.8612],
|
|
|
|
|
|
// South America
|
|
|
|
|
|
'São Paulo': [-23.5505, -46.6333], 'Rio de Janeiro': [-22.9068, -43.1729],
|
|
|
|
|
|
'Buenos Aires': [-34.6037, -58.3816], 'Bogotá': [4.7110, -74.0721],
|
|
|
|
|
|
'Lima': [-12.0464, -77.0428], 'Santiago': [-33.4489, -70.6693],
|
|
|
|
|
|
// Middle East & Africa
|
|
|
|
|
|
'Cairo': [30.0444, 31.2357], 'Dubai': [25.2048, 55.2708],
|
|
|
|
|
|
'Istanbul': [41.0082, 28.9784], 'Tel Aviv': [32.0853, 34.7818],
|
|
|
|
|
|
'Johannesburg': [26.2041, 28.0473], 'Lagos': [6.5244, 3.3792],
|
|
|
|
|
|
'Nairobi': [-1.2921, 36.8219], 'Cape Town': [-33.9249, 18.4241],
|
|
|
|
|
|
// Australia & Oceania
|
|
|
|
|
|
'Sydney': [-33.8688, 151.2093], 'Melbourne': [-37.8136, 144.9631],
|
|
|
|
|
|
'Brisbane': [-27.4698, 153.0251], 'Perth': [-31.9505, 115.8605],
|
|
|
|
|
|
'Auckland': [-36.8485, 174.7633],
|
|
|
|
|
|
// Additional cities
|
|
|
|
|
|
'Unknown': null
|
|
|
|
|
|
}};
|
|
|
|
|
|
|
|
|
|
|
|
// Country center coordinates (fallback when city not found)
|
2026-01-25 22:50:27 +01:00
|
|
|
|
const countryCoordinates = {{
|
|
|
|
|
|
'US': [37.1, -95.7], 'GB': [55.4, -3.4], 'CN': [35.9, 104.1], 'RU': [61.5, 105.3],
|
|
|
|
|
|
'JP': [36.2, 138.3], 'DE': [51.2, 10.5], 'FR': [46.6, 2.2], 'IN': [20.6, 78.96],
|
|
|
|
|
|
'BR': [-14.2, -51.9], 'CA': [56.1, -106.3], 'AU': [-25.3, 133.8], 'MX': [23.6, -102.6],
|
|
|
|
|
|
'ZA': [-30.6, 22.9], 'KR': [35.9, 127.8], 'IT': [41.9, 12.6], 'ES': [40.5, -3.7],
|
|
|
|
|
|
'NL': [52.1, 5.3], 'SE': [60.1, 18.6], 'CH': [46.8, 8.2], 'PL': [51.9, 19.1],
|
|
|
|
|
|
'SG': [1.4, 103.8], 'HK': [22.4, 114.1], 'TW': [23.7, 120.96], 'TH': [15.9, 100.9],
|
|
|
|
|
|
'VN': [14.1, 108.8], 'ID': [-0.8, 113.2], 'PH': [12.9, 121.8], 'MY': [4.2, 101.7],
|
|
|
|
|
|
'PK': [30.4, 69.2], 'BD': [23.7, 90.4], 'NG': [9.1, 8.7], 'EG': [26.8, 30.8],
|
|
|
|
|
|
'TR': [38.9, 35.2], 'IR': [32.4, 53.7], 'AE': [23.4, 53.8], 'KZ': [48.0, 66.9],
|
|
|
|
|
|
'UA': [48.4, 31.2], 'BG': [42.7, 25.5], 'RO': [45.9, 24.97], 'CZ': [49.8, 15.5],
|
|
|
|
|
|
'HU': [47.2, 19.5], 'AT': [47.5, 14.6], 'BE': [50.5, 4.5], 'DK': [56.3, 9.5],
|
2026-01-27 16:56:34 +01:00
|
|
|
|
'FI': [61.9, 25.8], 'NO': [60.5, 8.5], 'GR': [39.1, 21.8], 'PT': [39.4, -8.2],
|
|
|
|
|
|
'AR': [-38.4161, -63.6167], 'CO': [4.5709, -74.2973], 'CL': [-35.6751, -71.5430],
|
|
|
|
|
|
'PE': [-9.1900, -75.0152], 'VE': [6.4238, -66.5897], 'LS': [40.0, -100.0]
|
2026-01-25 22:50:27 +01:00
|
|
|
|
}};
|
|
|
|
|
|
|
2026-01-27 16:56:34 +01:00
|
|
|
|
// Helper function to get coordinates for an IP
|
|
|
|
|
|
function getIPCoordinates(ip) {{
|
2026-01-29 11:55:06 +01:00
|
|
|
|
// Use actual latitude and longitude if available
|
|
|
|
|
|
if (ip.latitude != null && ip.longitude != null) {{
|
|
|
|
|
|
return [ip.latitude, ip.longitude];
|
|
|
|
|
|
}}
|
|
|
|
|
|
// Fall back to city lookup
|
2026-01-27 16:56:34 +01:00
|
|
|
|
if (ip.city && cityCoordinates[ip.city]) {{
|
|
|
|
|
|
return cityCoordinates[ip.city];
|
|
|
|
|
|
}}
|
|
|
|
|
|
// Fall back to country
|
|
|
|
|
|
if (ip.country_code && countryCoordinates[ip.country_code]) {{
|
|
|
|
|
|
return countryCoordinates[ip.country_code];
|
|
|
|
|
|
}}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
// Track used coordinates to add small offsets for overlapping markers
|
|
|
|
|
|
const usedCoordinates = {{}};
|
|
|
|
|
|
function getUniqueCoordinates(baseCoords) {{
|
|
|
|
|
|
const key = `${{baseCoords[0].toFixed(4)}},${{baseCoords[1].toFixed(4)}}`;
|
|
|
|
|
|
if (!usedCoordinates[key]) {{
|
|
|
|
|
|
usedCoordinates[key] = 0;
|
|
|
|
|
|
}}
|
|
|
|
|
|
usedCoordinates[key]++;
|
|
|
|
|
|
|
|
|
|
|
|
// If this is the first marker at this location, use exact coordinates
|
|
|
|
|
|
if (usedCoordinates[key] === 1) {{
|
|
|
|
|
|
return baseCoords;
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
// Add small random offset for subsequent markers
|
|
|
|
|
|
// Offset increases with each marker to create a spread pattern
|
|
|
|
|
|
const angle = (usedCoordinates[key] * 137.5) % 360; // Golden angle for even distribution
|
|
|
|
|
|
const distance = 0.05 * Math.sqrt(usedCoordinates[key]); // Increase distance with more markers
|
|
|
|
|
|
const latOffset = distance * Math.cos(angle * Math.PI / 180);
|
|
|
|
|
|
const lngOffset = distance * Math.sin(angle * Math.PI / 180);
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
baseCoords[0] + latOffset,
|
|
|
|
|
|
baseCoords[1] + lngOffset
|
|
|
|
|
|
];
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
2026-01-26 12:36:22 +01:00
|
|
|
|
// Create layer groups for each category
|
|
|
|
|
|
markerLayers = {{
|
|
|
|
|
|
attacker: L.featureGroup(),
|
|
|
|
|
|
bad_crawler: L.featureGroup(),
|
|
|
|
|
|
good_crawler: L.featureGroup(),
|
|
|
|
|
|
regular_user: L.featureGroup(),
|
|
|
|
|
|
unknown: L.featureGroup()
|
|
|
|
|
|
}};
|
|
|
|
|
|
|
|
|
|
|
|
// Add markers for each IP
|
|
|
|
|
|
allIps.slice(0, 100).forEach(ip => {{
|
|
|
|
|
|
if (!ip.country_code || !ip.category) return;
|
2026-01-25 22:50:27 +01:00
|
|
|
|
|
2026-01-27 16:56:34 +01:00
|
|
|
|
// Get coordinates (city first, then country)
|
|
|
|
|
|
const baseCoords = getIPCoordinates(ip);
|
|
|
|
|
|
if (!baseCoords) return;
|
|
|
|
|
|
|
|
|
|
|
|
// Get unique coordinates with offset to prevent overlap
|
|
|
|
|
|
const coords = getUniqueCoordinates(baseCoords);
|
2026-01-25 22:50:27 +01:00
|
|
|
|
|
2026-01-26 12:36:22 +01:00
|
|
|
|
const category = ip.category.toLowerCase();
|
|
|
|
|
|
if (!markerLayers[category]) return;
|
|
|
|
|
|
|
2026-01-29 11:55:06 +01:00
|
|
|
|
// Calculate marker size based on request count with more dramatic scaling
|
|
|
|
|
|
// Scale up to 10,000 requests, then cap it
|
|
|
|
|
|
const requestsForScale = Math.min(ip.total_requests, 10000);
|
|
|
|
|
|
const sizeRatio = Math.pow(requestsForScale / 10000, 0.5); // Square root for better visual scaling
|
|
|
|
|
|
const markerSize = Math.max(10, Math.min(30, 10 + (sizeRatio * 20)));
|
2026-01-25 22:50:27 +01:00
|
|
|
|
|
2026-01-26 12:36:22 +01:00
|
|
|
|
// Create custom marker element with category-specific class
|
2026-01-25 22:50:27 +01:00
|
|
|
|
const markerElement = document.createElement('div');
|
2026-01-26 12:36:22 +01:00
|
|
|
|
markerElement.className = `ip-marker marker-${{category}}`;
|
2026-01-25 22:50:27 +01:00
|
|
|
|
markerElement.style.width = markerSize + 'px';
|
|
|
|
|
|
markerElement.style.height = markerSize + 'px';
|
|
|
|
|
|
markerElement.style.fontSize = (markerSize * 0.5) + 'px';
|
|
|
|
|
|
markerElement.textContent = '●';
|
|
|
|
|
|
|
|
|
|
|
|
const marker = L.marker(coords, {{
|
|
|
|
|
|
icon: L.divIcon({{
|
2026-01-27 16:56:34 +01:00
|
|
|
|
html: markerElement.outerHTML,
|
2026-01-25 22:50:27 +01:00
|
|
|
|
iconSize: [markerSize, markerSize],
|
2026-01-26 12:36:22 +01:00
|
|
|
|
className: `ip-custom-marker category-${{category}}`
|
2026-01-25 22:50:27 +01:00
|
|
|
|
}})
|
|
|
|
|
|
}});
|
|
|
|
|
|
|
2026-01-29 11:55:06 +01:00
|
|
|
|
// Create popup with category badge and chart
|
2026-01-26 12:36:22 +01:00
|
|
|
|
const categoryColor = categoryColors[category] || '#8b949e';
|
|
|
|
|
|
const categoryLabels = {{
|
|
|
|
|
|
attacker: 'Attacker',
|
|
|
|
|
|
bad_crawler: 'Bad Crawler',
|
|
|
|
|
|
good_crawler: 'Good Crawler',
|
|
|
|
|
|
regular_user: 'Regular User',
|
|
|
|
|
|
unknown: 'Unknown'
|
|
|
|
|
|
}};
|
|
|
|
|
|
|
2026-01-29 11:55:06 +01:00
|
|
|
|
// Bind popup once when marker is created
|
|
|
|
|
|
marker.bindPopup('', {{
|
|
|
|
|
|
maxWidth: 550,
|
|
|
|
|
|
className: 'ip-detail-popup'
|
|
|
|
|
|
}});
|
|
|
|
|
|
|
|
|
|
|
|
// Add click handler to fetch data and show popup
|
|
|
|
|
|
marker.on('click', async function(e) {{
|
|
|
|
|
|
// Show loading popup first
|
|
|
|
|
|
const loadingPopup = `
|
|
|
|
|
|
<div style="padding: 12px; min-width: 280px; max-width: 320px;">
|
|
|
|
|
|
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;">
|
|
|
|
|
|
<strong style="color: #58a6ff; font-size: 14px;">${{ip.ip}}</strong>
|
|
|
|
|
|
<span style="background: ${{categoryColor}}1a; color: ${{categoryColor}}; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: 600;">
|
|
|
|
|
|
${{categoryLabels[category]}}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style="text-align: center; padding: 20px; color: #8b949e;">
|
|
|
|
|
|
<div style="font-size: 12px;">Loading details...</div>
|
|
|
|
|
|
</div>
|
2026-01-25 22:50:27 +01:00
|
|
|
|
</div>
|
2026-01-29 11:55:06 +01:00
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
marker.setPopupContent(loadingPopup);
|
|
|
|
|
|
marker.openPopup();
|
|
|
|
|
|
|
|
|
|
|
|
try {{
|
|
|
|
|
|
console.log('Fetching IP stats for:', ip.ip);
|
|
|
|
|
|
const response = await fetch(`${{DASHBOARD_PATH}}/api/ip-stats/${{ip.ip}}`);
|
|
|
|
|
|
if (!response.ok) throw new Error('Failed to fetch IP stats');
|
|
|
|
|
|
|
|
|
|
|
|
const stats = await response.json();
|
|
|
|
|
|
console.log('Received stats:', stats);
|
|
|
|
|
|
|
|
|
|
|
|
// Build complete popup content with chart
|
|
|
|
|
|
let popupContent = `
|
|
|
|
|
|
<div style="padding: 12px; min-width: 200px;">
|
|
|
|
|
|
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;">
|
|
|
|
|
|
<strong style="color: #58a6ff; font-size: 14px;">${{ip.ip}}</strong>
|
|
|
|
|
|
<span style="background: ${{categoryColor}}1a; color: ${{categoryColor}}; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: 600;">
|
|
|
|
|
|
${{categoryLabels[category]}}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span style="color: #8b949e; font-size: 12px;">
|
|
|
|
|
|
${{ip.city ? (ip.country_code ? `${{ip.city}}, ${{ip.country_code}}` : ip.city) : (ip.country_code || 'Unknown')}}
|
|
|
|
|
|
</span><br/>
|
|
|
|
|
|
<div style="margin-top: 8px; border-top: 1px solid #30363d; padding-top: 8px;">
|
|
|
|
|
|
<div style="margin-bottom: 4px;"><span style="color: #8b949e;">Requests:</span> <span style="color: ${{categoryColor}}; font-weight: bold;">${{ip.total_requests}}</span></div>
|
|
|
|
|
|
<div style="margin-bottom: 4px;"><span style="color: #8b949e;">First Seen:</span> <span style="color: #58a6ff; font-size: 11px;">${{formatTimestamp(ip.first_seen)}}</span></div>
|
|
|
|
|
|
<div style="margin-bottom: 4px;"><span style="color: #8b949e;">Last Seen:</span> <span style="color: #58a6ff; font-size: 11px;">${{formatTimestamp(ip.last_seen)}}</span></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// Add chart if category scores exist
|
|
|
|
|
|
if (stats.category_scores && Object.keys(stats.category_scores).length > 0) {{
|
|
|
|
|
|
console.log('Category scores found:', stats.category_scores);
|
|
|
|
|
|
const chartHtml = generateMapPanelRadarChart(stats.category_scores);
|
|
|
|
|
|
console.log('Generated chart HTML length:', chartHtml.length);
|
|
|
|
|
|
popupContent += `
|
|
|
|
|
|
<div style="margin-top: 12px; border-top: 1px solid #30363d; padding-top: 12px;">
|
|
|
|
|
|
${{chartHtml}}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
popupContent += '</div>';
|
|
|
|
|
|
|
|
|
|
|
|
// Update popup content
|
|
|
|
|
|
console.log('Updating popup content');
|
|
|
|
|
|
marker.setPopupContent(popupContent);
|
|
|
|
|
|
}} catch (err) {{
|
|
|
|
|
|
console.error('Error fetching IP stats:', err);
|
|
|
|
|
|
const errorPopup = `
|
|
|
|
|
|
<div style="padding: 12px; min-width: 280px; max-width: 320px;">
|
|
|
|
|
|
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;">
|
|
|
|
|
|
<strong style="color: #58a6ff; font-size: 14px;">${{ip.ip}}</strong>
|
|
|
|
|
|
<span style="background: ${{categoryColor}}1a; color: ${{categoryColor}}; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: 600;">
|
|
|
|
|
|
${{categoryLabels[category]}}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span style="color: #8b949e; font-size: 12px;">
|
|
|
|
|
|
${{ip.city ? (ip.country_code ? `${{ip.city}}, ${{ip.country_code}}` : ip.city) : (ip.country_code || 'Unknown')}}
|
|
|
|
|
|
</span><br/>
|
|
|
|
|
|
<div style="margin-top: 8px; border-top: 1px solid #30363d; padding-top: 8px;">
|
|
|
|
|
|
<div style="margin-bottom: 4px;"><span style="color: #8b949e;">Requests:</span> <span style="color: ${{categoryColor}}; font-weight: bold;">${{ip.total_requests}}</span></div>
|
|
|
|
|
|
<div style="margin-bottom: 4px;"><span style="color: #8b949e;">First Seen:</span> <span style="color: #58a6ff; font-size: 11px;">${{formatTimestamp(ip.first_seen)}}</span></div>
|
|
|
|
|
|
<div style="margin-bottom: 4px;"><span style="color: #8b949e;">Last Seen:</span> <span style="color: #58a6ff; font-size: 11px;">${{formatTimestamp(ip.last_seen)}}</span></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style="margin-top: 12px; border-top: 1px solid #30363d; padding-top: 12px; text-align: center; color: #f85149; font-size: 11px;">
|
|
|
|
|
|
Failed to load chart: ${{err.message}}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
marker.setPopupContent(errorPopup);
|
|
|
|
|
|
}}
|
|
|
|
|
|
}});
|
2026-01-25 22:50:27 +01:00
|
|
|
|
|
2026-01-26 12:36:22 +01:00
|
|
|
|
markerLayers[category].addLayer(marker);
|
2026-01-25 22:50:27 +01:00
|
|
|
|
}});
|
|
|
|
|
|
|
2026-01-27 16:56:34 +01:00
|
|
|
|
// Add all marker layers to map initially
|
2026-01-26 12:36:22 +01:00
|
|
|
|
Object.values(markerLayers).forEach(layer => attackerMap.addLayer(layer));
|
|
|
|
|
|
|
|
|
|
|
|
// Fit map to all markers
|
|
|
|
|
|
const allMarkers = Object.values(markerLayers).reduce((acc, layer) => {{
|
|
|
|
|
|
acc.push(...layer.getLayers());
|
|
|
|
|
|
return acc;
|
|
|
|
|
|
}}, []);
|
|
|
|
|
|
|
|
|
|
|
|
if (allMarkers.length > 0) {{
|
|
|
|
|
|
const bounds = L.featureGroup(allMarkers).getBounds();
|
|
|
|
|
|
attackerMap.fitBounds(bounds, {{ padding: [50, 50] }});
|
2026-01-25 22:50:27 +01:00
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
}} catch (err) {{
|
|
|
|
|
|
console.error('Error initializing attacker map:', err);
|
|
|
|
|
|
mapContainer.innerHTML = '<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #f85149;">Failed to load map: ' + err.message + '</div>';
|
|
|
|
|
|
}}
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
2026-01-26 12:36:22 +01:00
|
|
|
|
// Update map filters based on checkbox selection
|
|
|
|
|
|
function updateMapFilters() {{
|
|
|
|
|
|
if (!attackerMap) return;
|
|
|
|
|
|
|
|
|
|
|
|
const filters = {{
|
|
|
|
|
|
attacker: document.getElementById('filter-attacker').checked,
|
|
|
|
|
|
bad_crawler: document.getElementById('filter-bad-crawler').checked,
|
|
|
|
|
|
good_crawler: document.getElementById('filter-good-crawler').checked,
|
|
|
|
|
|
regular_user: document.getElementById('filter-regular-user').checked,
|
|
|
|
|
|
unknown: document.getElementById('filter-unknown').checked
|
|
|
|
|
|
}};
|
|
|
|
|
|
|
|
|
|
|
|
// Update marker and circle layers visibility
|
|
|
|
|
|
Object.entries(filters).forEach(([category, show]) => {{
|
|
|
|
|
|
if (markerLayers[category]) {{
|
|
|
|
|
|
if (show) {{
|
|
|
|
|
|
if (!attackerMap.hasLayer(markerLayers[category])) {{
|
|
|
|
|
|
attackerMap.addLayer(markerLayers[category]);
|
|
|
|
|
|
}}
|
|
|
|
|
|
}} else {{
|
|
|
|
|
|
if (attackerMap.hasLayer(markerLayers[category])) {{
|
|
|
|
|
|
attackerMap.removeLayer(markerLayers[category]);
|
|
|
|
|
|
}}
|
|
|
|
|
|
}}
|
|
|
|
|
|
}}
|
|
|
|
|
|
}});
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
2026-01-25 22:50:27 +01:00
|
|
|
|
// Initialize map when Attacks tab is opened
|
|
|
|
|
|
const originalSwitchTab = window.switchTab;
|
|
|
|
|
|
let attackTypesChartLoaded = false;
|
|
|
|
|
|
|
|
|
|
|
|
window.switchTab = function(tabName) {{
|
|
|
|
|
|
originalSwitchTab(tabName);
|
|
|
|
|
|
if (tabName === 'ip-stats') {{
|
|
|
|
|
|
if (!attackerMap) {{
|
|
|
|
|
|
setTimeout(() => {{
|
|
|
|
|
|
initializeAttackerMap();
|
|
|
|
|
|
}}, 100);
|
|
|
|
|
|
}}
|
|
|
|
|
|
if (!attackTypesChartLoaded) {{
|
|
|
|
|
|
setTimeout(() => {{
|
|
|
|
|
|
loadAttackTypesChart();
|
|
|
|
|
|
}}, 100);
|
|
|
|
|
|
}}
|
|
|
|
|
|
}}
|
|
|
|
|
|
}};
|
|
|
|
|
|
|
|
|
|
|
|
// Load and render attack types bar chart
|
|
|
|
|
|
let attackTypesChart = null;
|
|
|
|
|
|
async function loadAttackTypesChart() {{
|
|
|
|
|
|
try {{
|
|
|
|
|
|
const canvas = document.getElementById('attack-types-chart');
|
|
|
|
|
|
if (!canvas) return;
|
|
|
|
|
|
|
|
|
|
|
|
const response = await fetch(DASHBOARD_PATH + '/api/attack-types?page=1&page_size=100', {{
|
|
|
|
|
|
cache: 'no-store',
|
|
|
|
|
|
headers: {{
|
|
|
|
|
|
'Cache-Control': 'no-cache',
|
|
|
|
|
|
'Pragma': 'no-cache'
|
|
|
|
|
|
}}
|
|
|
|
|
|
}});
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) throw new Error('Failed to fetch attack types');
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
const attacks = data.attacks || [];
|
|
|
|
|
|
|
|
|
|
|
|
if (attacks.length === 0) {{
|
|
|
|
|
|
canvas.style.display = 'none';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
// Aggregate attack types
|
|
|
|
|
|
const attackCounts = {{}};
|
|
|
|
|
|
attacks.forEach(attack => {{
|
|
|
|
|
|
if (attack.attack_types && Array.isArray(attack.attack_types)) {{
|
|
|
|
|
|
attack.attack_types.forEach(type => {{
|
|
|
|
|
|
attackCounts[type] = (attackCounts[type] || 0) + 1;
|
|
|
|
|
|
}});
|
|
|
|
|
|
}}
|
|
|
|
|
|
}});
|
|
|
|
|
|
|
|
|
|
|
|
// Sort and get top 10
|
|
|
|
|
|
const sortedAttacks = Object.entries(attackCounts)
|
|
|
|
|
|
.sort((a, b) => b[1] - a[1])
|
|
|
|
|
|
.slice(0, 10);
|
|
|
|
|
|
|
|
|
|
|
|
if (sortedAttacks.length === 0) {{
|
|
|
|
|
|
canvas.style.display = 'none';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
const labels = sortedAttacks.map(([type]) => type);
|
|
|
|
|
|
const counts = sortedAttacks.map(([, count]) => count);
|
2026-01-29 11:55:06 +01:00
|
|
|
|
const maxCount = Math.max(...counts);
|
2026-01-25 22:50:27 +01:00
|
|
|
|
|
2026-01-29 11:55:06 +01:00
|
|
|
|
// Enhanced color palette with gradients
|
2026-01-25 22:50:27 +01:00
|
|
|
|
const colorMap = {{
|
2026-01-29 11:55:06 +01:00
|
|
|
|
'SQL Injection': 'rgba(233, 105, 113, 0.85)',
|
|
|
|
|
|
'XSS': 'rgba(240, 136, 62, 0.85)',
|
|
|
|
|
|
'Directory Traversal': 'rgba(248, 150, 56, 0.85)',
|
|
|
|
|
|
'Command Injection': 'rgba(229, 229, 16, 0.85)',
|
|
|
|
|
|
'Path Traversal': 'rgba(123, 201, 71, 0.85)',
|
|
|
|
|
|
'Malware': 'rgba(88, 166, 255, 0.85)',
|
|
|
|
|
|
'Brute Force': 'rgba(79, 161, 246, 0.85)',
|
|
|
|
|
|
'DDoS': 'rgba(139, 148, 244, 0.85)',
|
|
|
|
|
|
'CSRF': 'rgba(188, 140, 258, 0.85)',
|
|
|
|
|
|
'File Upload': 'rgba(241, 107, 223, 0.85)'
|
2026-01-25 22:50:27 +01:00
|
|
|
|
}};
|
|
|
|
|
|
|
2026-01-29 11:55:06 +01:00
|
|
|
|
const borderColorMap = {{
|
|
|
|
|
|
'SQL Injection': 'rgba(233, 105, 113, 1)',
|
|
|
|
|
|
'XSS': 'rgba(240, 136, 62, 1)',
|
|
|
|
|
|
'Directory Traversal': 'rgba(248, 150, 56, 1)',
|
|
|
|
|
|
'Command Injection': 'rgba(229, 229, 16, 1)',
|
|
|
|
|
|
'Path Traversal': 'rgba(123, 201, 71, 1)',
|
|
|
|
|
|
'Malware': 'rgba(88, 166, 255, 1)',
|
|
|
|
|
|
'Brute Force': 'rgba(79, 161, 246, 1)',
|
|
|
|
|
|
'DDoS': 'rgba(139, 148, 244, 1)',
|
|
|
|
|
|
'CSRF': 'rgba(188, 140, 258, 1)',
|
|
|
|
|
|
'File Upload': 'rgba(241, 107, 223, 1)'
|
|
|
|
|
|
}};
|
|
|
|
|
|
|
|
|
|
|
|
const hoverColorMap = {{
|
|
|
|
|
|
'SQL Injection': 'rgba(233, 105, 113, 1)',
|
|
|
|
|
|
'XSS': 'rgba(240, 136, 62, 1)',
|
|
|
|
|
|
'Directory Traversal': 'rgba(248, 150, 56, 1)',
|
|
|
|
|
|
'Command Injection': 'rgba(229, 229, 16, 1)',
|
|
|
|
|
|
'Path Traversal': 'rgba(123, 201, 71, 1)',
|
|
|
|
|
|
'Malware': 'rgba(88, 166, 255, 1)',
|
|
|
|
|
|
'Brute Force': 'rgba(79, 161, 246, 1)',
|
|
|
|
|
|
'DDoS': 'rgba(139, 148, 244, 1)',
|
|
|
|
|
|
'CSRF': 'rgba(188, 140, 258, 1)',
|
|
|
|
|
|
'File Upload': 'rgba(241, 107, 223, 1)'
|
|
|
|
|
|
}};
|
|
|
|
|
|
|
|
|
|
|
|
const backgroundColors = labels.map(label => colorMap[label] || 'rgba(88, 166, 255, 0.85)');
|
|
|
|
|
|
const borderColors = labels.map(label => borderColorMap[label] || 'rgba(88, 166, 255, 1)');
|
|
|
|
|
|
const hoverColors = labels.map(label => hoverColorMap[label] || 'rgba(88, 166, 255, 1)');
|
2026-01-25 22:50:27 +01:00
|
|
|
|
|
|
|
|
|
|
// Create or update chart
|
|
|
|
|
|
if (attackTypesChart) {{
|
|
|
|
|
|
attackTypesChart.destroy();
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
|
|
attackTypesChart = new Chart(ctx, {{
|
|
|
|
|
|
type: 'bar',
|
|
|
|
|
|
data: {{
|
|
|
|
|
|
labels: labels,
|
|
|
|
|
|
datasets: [{{
|
|
|
|
|
|
data: counts,
|
|
|
|
|
|
backgroundColor: backgroundColors,
|
|
|
|
|
|
borderColor: borderColors,
|
2026-01-29 11:55:06 +01:00
|
|
|
|
borderWidth: 2,
|
|
|
|
|
|
borderRadius: 6,
|
|
|
|
|
|
borderSkipped: false
|
2026-01-25 22:50:27 +01:00
|
|
|
|
}}]
|
|
|
|
|
|
}},
|
|
|
|
|
|
options: {{
|
|
|
|
|
|
responsive: true,
|
|
|
|
|
|
maintainAspectRatio: false,
|
|
|
|
|
|
indexAxis: 'y',
|
|
|
|
|
|
plugins: {{
|
|
|
|
|
|
legend: {{
|
|
|
|
|
|
display: false
|
|
|
|
|
|
}},
|
|
|
|
|
|
tooltip: {{
|
2026-01-29 11:55:06 +01:00
|
|
|
|
enabled: true,
|
|
|
|
|
|
backgroundColor: 'rgba(22, 27, 34, 0.95)',
|
2026-01-25 22:50:27 +01:00
|
|
|
|
titleColor: '#58a6ff',
|
|
|
|
|
|
bodyColor: '#c9d1d9',
|
2026-01-29 11:55:06 +01:00
|
|
|
|
borderColor: '#58a6ff',
|
|
|
|
|
|
borderWidth: 2,
|
|
|
|
|
|
padding: 14,
|
|
|
|
|
|
displayColors: false,
|
2026-01-25 22:50:27 +01:00
|
|
|
|
titleFont: {{
|
2026-01-29 11:55:06 +01:00
|
|
|
|
size: 14,
|
|
|
|
|
|
weight: 'bold',
|
|
|
|
|
|
family: "'Segoe UI', Tahoma, Geneva, Verdana"
|
2026-01-25 22:50:27 +01:00
|
|
|
|
}},
|
|
|
|
|
|
bodyFont: {{
|
2026-01-29 11:55:06 +01:00
|
|
|
|
size: 13,
|
|
|
|
|
|
family: "'Segoe UI', Tahoma, Geneva, Verdana"
|
2026-01-25 22:50:27 +01:00
|
|
|
|
}},
|
2026-01-29 11:55:06 +01:00
|
|
|
|
caretSize: 8,
|
|
|
|
|
|
caretPadding: 12,
|
2026-01-25 22:50:27 +01:00
|
|
|
|
callbacks: {{
|
2026-01-29 11:55:06 +01:00
|
|
|
|
title: function(context) {{
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}},
|
2026-01-25 22:50:27 +01:00
|
|
|
|
label: function(context) {{
|
2026-01-29 11:55:06 +01:00
|
|
|
|
return context.parsed.x;
|
2026-01-25 22:50:27 +01:00
|
|
|
|
}}
|
|
|
|
|
|
}}
|
|
|
|
|
|
}}
|
|
|
|
|
|
}},
|
|
|
|
|
|
scales: {{
|
|
|
|
|
|
x: {{
|
|
|
|
|
|
beginAtZero: true,
|
|
|
|
|
|
ticks: {{
|
|
|
|
|
|
color: '#8b949e',
|
|
|
|
|
|
font: {{
|
2026-01-29 11:55:06 +01:00
|
|
|
|
size: 12,
|
|
|
|
|
|
weight: '500'
|
2026-01-25 22:50:27 +01:00
|
|
|
|
}}
|
|
|
|
|
|
}},
|
|
|
|
|
|
grid: {{
|
2026-01-29 11:55:06 +01:00
|
|
|
|
color: 'rgba(48, 54, 61, 0.4)',
|
|
|
|
|
|
drawBorder: false,
|
|
|
|
|
|
drawTicks: false
|
2026-01-25 22:50:27 +01:00
|
|
|
|
}}
|
|
|
|
|
|
}},
|
|
|
|
|
|
y: {{
|
|
|
|
|
|
ticks: {{
|
|
|
|
|
|
color: '#c9d1d9',
|
|
|
|
|
|
font: {{
|
2026-01-29 11:55:06 +01:00
|
|
|
|
size: 13,
|
|
|
|
|
|
weight: '600'
|
2026-01-25 22:50:27 +01:00
|
|
|
|
}},
|
2026-01-29 11:55:06 +01:00
|
|
|
|
padding: 12,
|
|
|
|
|
|
callback: function(value, index) {{
|
|
|
|
|
|
const label = this.getLabelForValue(value);
|
|
|
|
|
|
const maxLength = 25;
|
|
|
|
|
|
return label.length > maxLength ? label.substring(0, maxLength) + '…' : label;
|
|
|
|
|
|
}}
|
2026-01-25 22:50:27 +01:00
|
|
|
|
}},
|
|
|
|
|
|
grid: {{
|
|
|
|
|
|
display: false,
|
|
|
|
|
|
drawBorder: false
|
|
|
|
|
|
}}
|
|
|
|
|
|
}}
|
2026-01-29 11:55:06 +01:00
|
|
|
|
}},
|
|
|
|
|
|
animation: {{
|
|
|
|
|
|
duration: 1000,
|
|
|
|
|
|
easing: 'easeInOutQuart',
|
|
|
|
|
|
delay: (context) => {{
|
|
|
|
|
|
let delay = 0;
|
|
|
|
|
|
if (context.type === 'data') {{
|
|
|
|
|
|
delay = context.dataIndex * 50 + context.datasetIndex * 100;
|
|
|
|
|
|
}}
|
|
|
|
|
|
return delay;
|
|
|
|
|
|
}}
|
|
|
|
|
|
}},
|
|
|
|
|
|
onHover: (event, activeElements) => {{
|
|
|
|
|
|
canvas.style.cursor = activeElements.length > 0 ? 'pointer' : 'default';
|
2026-01-25 22:50:27 +01:00
|
|
|
|
}}
|
2026-01-29 11:55:06 +01:00
|
|
|
|
}},
|
|
|
|
|
|
plugins: [{{
|
|
|
|
|
|
id: 'customCanvasBackgroundColor',
|
|
|
|
|
|
beforeDraw: (chart) => {{
|
|
|
|
|
|
if (chart.ctx) {{
|
|
|
|
|
|
chart.ctx.save();
|
|
|
|
|
|
chart.ctx.globalCompositeOperation = 'destination-over';
|
|
|
|
|
|
chart.ctx.fillStyle = 'rgba(0,0,0,0)';
|
|
|
|
|
|
chart.ctx.fillRect(0, 0, chart.width, chart.height);
|
|
|
|
|
|
chart.ctx.restore();
|
|
|
|
|
|
}}
|
|
|
|
|
|
}}
|
|
|
|
|
|
}}]
|
2026-01-25 22:50:27 +01:00
|
|
|
|
}});
|
|
|
|
|
|
|
|
|
|
|
|
attackTypesChartLoaded = true;
|
|
|
|
|
|
}} catch (err) {{
|
|
|
|
|
|
console.error('Error loading attack types chart:', err);
|
|
|
|
|
|
}}
|
|
|
|
|
|
}}
|
2026-01-05 17:27:27 +01:00
|
|
|
|
</script>
|
2025-12-14 19:08:01 +01:00
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|
|
|
|
|
|
"""
|