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
|
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))
|
|
|
|
|
|
2025-12-28 17:07:18 +01:00
|
|
|
def format_timestamp(iso_timestamp: str) -> str:
|
|
|
|
|
"""Format ISO timestamp for display (YYYY-MM-DD HH:MM:SS)"""
|
|
|
|
|
try:
|
|
|
|
|
dt = datetime.fromisoformat(iso_timestamp)
|
|
|
|
|
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
|
except Exception:
|
|
|
|
|
# Fallback for old format
|
|
|
|
|
return iso_timestamp.split("T")[1][:8] if "T" in iso_timestamp else iso_timestamp
|
|
|
|
|
|
2025-12-14 19:08:01 +01:00
|
|
|
|
|
|
|
|
def generate_dashboard(stats: dict) -> str:
|
|
|
|
|
"""Generate dashboard HTML with access statistics"""
|
|
|
|
|
|
2025-12-28 10:43:32 -06:00
|
|
|
# Generate IP rows (IPs are generally safe but escape for consistency)
|
2025-12-14 19:08:01 +01:00
|
|
|
top_ips_rows = '\n'.join([
|
2025-12-28 10:43:32 -06:00
|
|
|
f'<tr><td class="rank">{i+1}</td><td>{_escape(ip)}</td><td>{count}</td></tr>'
|
2025-12-14 19:08:01 +01:00
|
|
|
for i, (ip, count) in enumerate(stats['top_ips'])
|
|
|
|
|
]) or '<tr><td colspan="3" style="text-align:center;">No data</td></tr>'
|
|
|
|
|
|
2025-12-28 10:43:32 -06:00
|
|
|
# Generate paths rows (CRITICAL: paths can contain XSS payloads)
|
2025-12-14 19:08:01 +01:00
|
|
|
top_paths_rows = '\n'.join([
|
2025-12-28 10:43:32 -06:00
|
|
|
f'<tr><td class="rank">{i+1}</td><td>{_escape(path)}</td><td>{count}</td></tr>'
|
2025-12-14 19:08:01 +01:00
|
|
|
for i, (path, count) in enumerate(stats['top_paths'])
|
|
|
|
|
]) or '<tr><td colspan="3" style="text-align:center;">No data</td></tr>'
|
|
|
|
|
|
2025-12-28 10:43:32 -06:00
|
|
|
# Generate User-Agent rows (CRITICAL: user agents can contain XSS payloads)
|
2025-12-14 19:08:01 +01:00
|
|
|
top_ua_rows = '\n'.join([
|
2025-12-28 10:43:32 -06:00
|
|
|
f'<tr><td class="rank">{i+1}</td><td style="word-break: break-all;">{_escape(ua[:80])}</td><td>{count}</td></tr>'
|
2025-12-14 19:08:01 +01:00
|
|
|
for i, (ua, count) in enumerate(stats['top_user_agents'])
|
|
|
|
|
]) or '<tr><td colspan="3" style="text-align:center;">No data</td></tr>'
|
|
|
|
|
|
2025-12-28 10:43:32 -06:00
|
|
|
# Generate suspicious accesses rows (CRITICAL: multiple user-controlled fields)
|
2025-12-14 19:08:01 +01:00
|
|
|
suspicious_rows = '\n'.join([
|
2025-12-28 10:43:32 -06:00
|
|
|
f'<tr><td>{_escape(log["ip"])}</td><td>{_escape(log["path"])}</td><td style="word-break: break-all;">{_escape(log["user_agent"][:60])}</td><td>{_escape(log["timestamp"].split("T")[1][:8])}</td></tr>'
|
2025-12-14 19:08:01 +01:00
|
|
|
for log in stats['recent_suspicious'][-10:]
|
|
|
|
|
]) or '<tr><td colspan="4" style="text-align:center;">No suspicious activity detected</td></tr>'
|
|
|
|
|
|
|
|
|
|
# Generate honeypot triggered IPs rows
|
|
|
|
|
honeypot_rows = '\n'.join([
|
2025-12-28 10:43:32 -06:00
|
|
|
f'<tr><td>{_escape(ip)}</td><td style="word-break: break-all;">{_escape(", ".join(paths))}</td><td>{len(paths)}</td></tr>'
|
2025-12-14 19:08:01 +01:00
|
|
|
for ip, paths in stats.get('honeypot_triggered_ips', [])
|
|
|
|
|
]) or '<tr><td colspan="3" style="text-align:center;">No honeypot triggers yet</td></tr>'
|
|
|
|
|
|
2025-12-28 10:43:32 -06:00
|
|
|
# Generate attack types rows (CRITICAL: paths and user agents are user-controlled)
|
2025-12-24 10:25:00 -06:00
|
|
|
attack_type_rows = '\n'.join([
|
2025-12-28 10:43:32 -06:00
|
|
|
f'<tr><td>{_escape(log["ip"])}</td><td>{_escape(log["path"])}</td><td>{_escape(", ".join(log["attack_types"]))}</td><td style="word-break: break-all;">{_escape(log["user_agent"][:60])}</td><td>{_escape(log["timestamp"].split("T")[1][:8])}</td></tr>'
|
2025-12-24 10:25:00 -06:00
|
|
|
for log in stats.get('attack_types', [])[-10:]
|
|
|
|
|
]) or '<tr><td colspan="4" style="text-align:center;">No attacks detected</td></tr>'
|
|
|
|
|
|
2025-12-28 10:43:32 -06:00
|
|
|
# Generate credential attempts rows (CRITICAL: usernames and passwords are user-controlled)
|
2025-12-27 19:17:27 +01:00
|
|
|
credential_rows = '\n'.join([
|
2025-12-28 10:43:32 -06:00
|
|
|
f'<tr><td>{_escape(log["ip"])}</td><td>{_escape(log["username"])}</td><td>{_escape(log["password"])}</td><td>{_escape(log["path"])}</td><td>{_escape(log["timestamp"].split("T")[1][:8])}</td></tr>'
|
2025-12-27 19:17:27 +01:00
|
|
|
for log in stats.get('credential_attempts', [])[-20:]
|
|
|
|
|
]) or '<tr><td colspan="5" style="text-align:center;">No credentials captured yet</td></tr>'
|
|
|
|
|
|
2025-12-14 19:08:01 +01:00
|
|
|
return f"""<!DOCTYPE html>
|
|
|
|
|
<html>
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
<title>Krawl Dashboard</title>
|
|
|
|
|
<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;
|
|
|
|
|
}}
|
|
|
|
|
h1 {{
|
|
|
|
|
color: #58a6ff;
|
|
|
|
|
text-align: center;
|
|
|
|
|
margin-bottom: 40px;
|
|
|
|
|
}}
|
|
|
|
|
.stats-grid {{
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
|
|
|
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;
|
|
|
|
|
}}
|
2025-12-14 19:08:01 +01:00
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<div class="container">
|
2026-01-05 17:27:27 +01:00
|
|
|
<h1>Krawl Dashboard</h1>
|
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>
|
2025-12-14 19:08:01 +01:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="table-container alert-section">
|
2026-01-05 17:27:27 +01:00
|
|
|
<h2>Honeypot Triggers by IP</h2>
|
|
|
|
|
<table id="honeypot-table">
|
2025-12-14 19:08:01 +01:00
|
|
|
<thead>
|
|
|
|
|
<tr>
|
2026-01-05 17:27:27 +01:00
|
|
|
<th class="sortable" data-sort="ip">IP Address</th>
|
2025-12-14 19:08:01 +01:00
|
|
|
<th>Accessed Paths</th>
|
2026-01-05 17:27:27 +01:00
|
|
|
<th class="sortable" data-sort="count">Count</th>
|
2025-12-14 19:08:01 +01:00
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{honeypot_rows}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<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-05 17:27:27 +01:00
|
|
|
<h2>Captured Credentials</h2>
|
2025-12-27 19:17:27 +01:00
|
|
|
<table>
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th>IP Address</th>
|
|
|
|
|
<th>Username</th>
|
|
|
|
|
<th>Password</th>
|
|
|
|
|
<th>Path</th>
|
|
|
|
|
<th>Time</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{credential_rows}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-12-24 10:25:00 -06:00
|
|
|
<div class="table-container alert-section">
|
2026-01-05 17:27:27 +01:00
|
|
|
<h2>Detected Attack Types</h2>
|
2025-12-24 10:25:00 -06:00
|
|
|
<table>
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th>IP Address</th>
|
|
|
|
|
<th>Path</th>
|
|
|
|
|
<th>Attack Types</th>
|
|
|
|
|
<th>User-Agent</th>
|
|
|
|
|
<th>Time</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{attack_type_rows}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-12-14 19:08:01 +01:00
|
|
|
<div class="table-container">
|
|
|
|
|
<h2>Top IP Addresses</h2>
|
|
|
|
|
<table>
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th>#</th>
|
|
|
|
|
<th>IP Address</th>
|
|
|
|
|
<th>Access Count</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{top_ips_rows}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="table-container">
|
|
|
|
|
<h2>Top Paths</h2>
|
|
|
|
|
<table>
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th>#</th>
|
|
|
|
|
<th>Path</th>
|
|
|
|
|
<th>Access Count</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{top_paths_rows}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="table-container">
|
|
|
|
|
<h2>Top User-Agents</h2>
|
|
|
|
|
<table>
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th>#</th>
|
|
|
|
|
<th>User-Agent</th>
|
|
|
|
|
<th>Count</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{top_ua_rows}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-01-05 17:27:27 +01:00
|
|
|
<script>
|
|
|
|
|
// Add sorting functionality to tables
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
// Determine sort direction
|
|
|
|
|
const isAscending = this.classList.contains('asc');
|
|
|
|
|
|
|
|
|
|
// Remove sort classes from all headers in this table
|
|
|
|
|
table.querySelectorAll('th.sortable').forEach(th => {{
|
|
|
|
|
th.classList.remove('asc', 'desc');
|
|
|
|
|
}});
|
|
|
|
|
|
|
|
|
|
// Add appropriate class to clicked header
|
|
|
|
|
this.classList.add(isAscending ? 'desc' : 'asc');
|
|
|
|
|
|
|
|
|
|
// Sort rows
|
|
|
|
|
rows.sort((a, b) => {{
|
|
|
|
|
let aValue = a.cells[columnIndex].textContent.trim();
|
|
|
|
|
let bValue = b.cells[columnIndex].textContent.trim();
|
|
|
|
|
|
|
|
|
|
// Handle numeric sorting
|
|
|
|
|
if (sortType === 'count') {{
|
|
|
|
|
aValue = parseInt(aValue) || 0;
|
|
|
|
|
bValue = parseInt(bValue) || 0;
|
|
|
|
|
return isAscending ? bValue - aValue : aValue - bValue;
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
// Handle IP address sorting
|
|
|
|
|
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;
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
// Default string sorting
|
|
|
|
|
if (isAscending) {{
|
|
|
|
|
return bValue.localeCompare(aValue);
|
|
|
|
|
}} else {{
|
|
|
|
|
return aValue.localeCompare(bValue);
|
|
|
|
|
}}
|
|
|
|
|
}});
|
|
|
|
|
|
|
|
|
|
// Re-append sorted rows
|
|
|
|
|
rows.forEach(row => tbody.appendChild(row));
|
|
|
|
|
}});
|
|
|
|
|
}});
|
|
|
|
|
</script>
|
2025-12-14 19:08:01 +01:00
|
|
|
</body>
|
|
|
|
|
</html>
|
|
|
|
|
"""
|