Merge pull request #44 from BlessedRebuS/fix/dashboard

Fix/dashboard
This commit is contained in:
Lorenzo Venerandi
2026-01-22 15:10:11 +01:00
committed by GitHub
2 changed files with 364 additions and 136 deletions

View File

@@ -395,101 +395,47 @@ def generate_dashboard(stats: dict, dashboard_path: str = '') -> str:
color: #8b949e;
border: 1px solid #8b949e;
}}
.timeline-container {{
.timeline-section {{
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #30363d;
}}
.timeline-title {{
color: #58a6ff;
font-size: 13px;
font-weight: 600;
.timeline-container {{
display: flex;
gap: 20px;
min-height: 200px;
}}
.timeline-column {{
flex: 1;
min-width: 0;
overflow: auto;
max-height: 350px;
}}
.timeline-column:first-child {{
flex: 1.5;
}}
.timeline-column:last-child {{
flex: 1;
}}
.timeline-header {{
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}}
.timeline {{
position: relative;
padding-left: 30px;
}}
.timeline::before {{
content: '';
position: absolute;
left: 12px;
top: 5px;
bottom: 5px;
width: 3px;
background: #30363d;
}}
.timeline-item {{
position: relative;
padding-bottom: 15px;
}}
.timeline-item:last-child {{
padding-bottom: 0;
}}
.timeline-marker {{
position: absolute;
left: -26px;
width: 16px;
height: 16px;
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;
}}
.timeline-content {{
font-size: 12px;
}}
.timeline-category {{
font-weight: 600;
}}
.timeline-timestamp {{
color: #8b949e;
font-size: 11px;
margin-top: 2px;
}}
.timeline-arrow {{
color: #8b949e;
margin: 0 7px;
}}
.reputation-container {{
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #30363d;
}}
.reputation-title {{
color: #58a6ff;
font-size: 13px;
font-weight: 600;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #30363d;
}}
.reputation-badges {{
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
.reputation-title {{
color: #8b949e;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
margin-bottom: 8px;
}}
.reputation-badge {{
display: inline-flex;
align-items: center;
gap: 4px;
gap: 3px;
padding: 4px 8px;
background: #161b22;
border: 1px solid #f851494d;
@@ -498,28 +444,60 @@ def generate_dashboard(stats: dict, dashboard_path: str = '') -> str:
color: #f85149;
text-decoration: none;
transition: all 0.2s;
margin-bottom: 6px;
margin-right: 6px;
white-space: nowrap;
}}
.reputation-badge:hover {{
background: #1c2128;
border-color: #f85149;
}}
.reputation-badge-icon {{
font-size: 12px;
}}
.reputation-clean {{
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
gap: 3px;
padding: 4px 8px;
background: #161b22;
border: 1px solid #3fb9504d;
border-radius: 4px;
font-size: 11px;
color: #3fb950;
margin-bottom: 6px;
}}
.reputation-clean-icon {{
font-size: 13px;
.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;
}}
.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; }}
</style>
</head>
@@ -838,71 +816,62 @@ def generate_dashboard(stats: dict, dashboard_path: str = '') -> str:
}}
if (stats.category_history && stats.category_history.length > 0) {{
html += '<div class="timeline-section">';
html += '<div class="timeline-container">';
html += '<div class="timeline-header">';
html += '<div class="timeline-title">Behavior Timeline</div>';
if (stats.list_on && Object.keys(stats.list_on).length > 0) {{
html += '<div class="reputation-badges">';
html += '<span class="reputation-title" style="margin-bottom:0; margin-right:4px;">Listed on</span>';
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="Listed on ${'{'}source{'}'}">`;
html += '<span class="reputation-badge-icon"></span>';
html += `<span>${{source}}</span>`;
html += '</a>';
}} else {{
html += '<span class="reputation-badge" style="cursor: default;" title="Listed on">';
html += '<span class="reputation-badge-icon"></span>';
html += `<span>${{source}}</span>`;
html += '</span>';
}}
}});
html += '</div>';
}} else if (stats.country_code || stats.asn) {{
html += '<div class="reputation-badges">';
html += '<span class="reputation-title" style="margin-bottom:0; margin-right:4px;">Reputation</span>';
html += '<span class="reputation-clean" title="Not found on public blacklists">';
html += '<span class="reputation-clean-icon">✓</span>';
html += '<span>Clean</span>';
html += '</span>';
html += '</div>';
}}
html += '</div>';
// Timeline column
html += '<div class="timeline-column">';
html += '<div class="timeline-header">Behavior Timeline</div>';
html += '<div class="timeline">';
stats.category_history.forEach((change, index) => {{
stats.category_history.forEach(change => {{
const categoryClass = change.new_category.toLowerCase().replace('_', '-');
const timestamp = formatTimestamp(change.timestamp);
const oldClass = change.old_category ? 'category-' + change.old_category.toLowerCase().replace('_', '-') : '';
const newClass = 'category-' + categoryClass;
html += '<div class="timeline-item">';
html += `<div class="timeline-marker ${{categoryClass}}"></div>`;
html += '<div class="timeline-content">';
if (change.old_category) {{
const oldCategoryBadge = 'category-' + change.old_category.toLowerCase().replace('_', '-');
html += `<span class="category-badge ${{oldCategoryBadge}}">${{change.old_category}}</span>`;
html += '<span class="timeline-arrow">→</span>';
html += `<span class="category-badge ${{oldClass}}">${{change.old_category}}</span>`;
html += '<span style="color: #8b949e; margin: 0 4px;">→</span>';
}} else {{
html += '<span style="color: #8b949e;">Initial:</span> ';
html += '<span style="color: #8b949e;">Initial:</span>';
}}
const newCategoryBadge = 'category-' + change.new_category.toLowerCase().replace('_', '-');
html += `<span class="category-badge ${{newCategoryBadge}}">${{change.new_category}}</span>`;
html += `<div class="timeline-timestamp">${{timestamp}}</div>`;
html += `<span class="category-badge ${{newClass}}">${{change.new_category}}</span>`;
html += `<div class="timeline-time">${{timestamp}}</div>`;
html += '</div>';
html += '</div>';
}});
html += '</div>';
html += '</div>';
// 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>';
}}
html += '</div>';

View File

@@ -0,0 +1,259 @@
#!/usr/bin/env python3
"""
Test script to insert fake external IPs into the database for testing the dashboard.
This generates realistic-looking test data including access logs, credential attempts, and attack detections.
Also triggers category behavior changes to demonstrate the timeline feature.
"""
import random
import time
import sys
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from pathlib import Path
# Add parent src directory to path so we can import database and logger
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from database import get_database
from logger import get_app_logger
# ----------------------
# TEST DATA GENERATORS
# ----------------------
FAKE_IPS = [
"203.0.113.45", # Regular attacker IP
"198.51.100.89", # Credential harvester IP
"192.0.2.120", # Bot IP
"205.32.180.65", # Another attacker
"210.45.67.89", # Suspicious IP
"175.23.45.67", # International IP
"182.91.102.45", # Another suspicious IP
]
FAKE_PATHS = [
"/admin",
"/login",
"/admin/login",
"/api/users",
"/wp-admin",
"/.env",
"/config.php",
"/admin.php",
"/shell.php",
"/../../../etc/passwd",
"/sqlmap",
"/w00t.php",
"/shell",
"/joomla/administrator",
]
FAKE_USER_AGENTS = [
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
"Nmap Scripting Engine",
"curl/7.68.0",
"python-requests/2.28.1",
"sqlmap/1.6.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
"ZmEu",
"nikto/2.1.6",
]
FAKE_CREDENTIALS = [
("admin", "admin"),
("admin", "password"),
("root", "123456"),
("test", "test"),
("guest", "guest"),
("user", "12345"),
]
ATTACK_TYPES = [
"sql_injection",
"xss_attempt",
"path_traversal",
"suspicious_pattern",
"credential_submission",
]
CATEGORIES = [
"ATTACKER",
"BAD_CRAWLER",
"GOOD_CRAWLER",
"REGULAR_USER",
"UNKNOWN",
]
def generate_category_scores():
"""Generate random category scores."""
scores = {
"attacker": random.randint(0, 100),
"good_crawler": random.randint(0, 100),
"bad_crawler": random.randint(0, 100),
"regular_user": random.randint(0, 100),
"unknown": random.randint(0, 100),
}
return scores
def generate_analyzed_metrics():
"""Generate random analyzed metrics."""
return {
"request_frequency": random.uniform(0.1, 100.0),
"suspicious_patterns": random.randint(0, 20),
"credential_attempts": random.randint(0, 10),
"attack_diversity": random.uniform(0, 1.0),
}
def generate_fake_data(num_ips: int = 5, logs_per_ip: int = 15, credentials_per_ip: int = 3):
"""
Generate and insert fake test data into the database.
Args:
num_ips: Number of unique fake IPs to generate (default: 5)
logs_per_ip: Number of access logs per IP (default: 15)
credentials_per_ip: Number of credential attempts per IP (default: 3)
"""
db_manager = get_database()
app_logger = get_app_logger()
# Ensure database is initialized
if not db_manager._initialized:
db_manager.initialize()
app_logger.info("=" * 60)
app_logger.info("Starting fake IP data generation for testing")
app_logger.info("=" * 60)
total_logs = 0
total_credentials = 0
total_attacks = 0
total_category_changes = 0
# Select random IPs from the pool
selected_ips = random.sample(FAKE_IPS, min(num_ips, len(FAKE_IPS)))
for ip in selected_ips:
app_logger.info(f"\nGenerating data for IP: {ip}")
# Generate access logs for this IP
for _ in range(logs_per_ip):
path = random.choice(FAKE_PATHS)
user_agent = random.choice(FAKE_USER_AGENTS)
is_suspicious = random.choice([True, False, False]) # 33% chance of suspicious
is_honeypot = random.choice([True, False, False, False]) # 25% chance of honeypot trigger
# Randomly decide if this log has attack detections
attack_types = None
if random.choice([True, False, False]): # 33% chance
num_attacks = random.randint(1, 3)
attack_types = random.sample(ATTACK_TYPES, num_attacks)
log_id = db_manager.persist_access(
ip=ip,
path=path,
user_agent=user_agent,
method=random.choice(["GET", "POST"]),
is_suspicious=is_suspicious,
is_honeypot_trigger=is_honeypot,
attack_types=attack_types,
)
if log_id:
total_logs += 1
if attack_types:
total_attacks += len(attack_types)
# Generate credential attempts for this IP
for _ in range(credentials_per_ip):
username, password = random.choice(FAKE_CREDENTIALS)
path = random.choice(["/login", "/admin/login", "/api/auth"])
cred_id = db_manager.persist_credential(
ip=ip,
path=path,
username=username,
password=password,
)
if cred_id:
total_credentials += 1
app_logger.info(f" ✓ Generated {logs_per_ip} access logs")
app_logger.info(f" ✓ Generated {credentials_per_ip} credential attempts")
# Trigger behavior/category changes to demonstrate timeline feature
# First analysis
initial_category = random.choice(CATEGORIES)
app_logger.info(f" ⟳ Analyzing behavior - Initial category: {initial_category}")
db_manager.update_ip_stats_analysis(
ip=ip,
analyzed_metrics=generate_analyzed_metrics(),
category=initial_category,
category_scores=generate_category_scores(),
last_analysis=datetime.now(tz=ZoneInfo('UTC'))
)
total_category_changes += 1
# Small delay to ensure timestamps are different
time.sleep(0.1)
# Second analysis with potential category change (70% chance)
if random.random() < 0.7:
new_category = random.choice([c for c in CATEGORIES if c != initial_category])
app_logger.info(f" ⟳ Behavior change detected: {initial_category}{new_category}")
db_manager.update_ip_stats_analysis(
ip=ip,
analyzed_metrics=generate_analyzed_metrics(),
category=new_category,
category_scores=generate_category_scores(),
last_analysis=datetime.now(tz=ZoneInfo('UTC'))
)
total_category_changes += 1
# Optional third change (40% chance)
if random.random() < 0.4:
final_category = random.choice([c for c in CATEGORIES if c != new_category])
app_logger.info(f" ⟳ Another behavior change: {new_category}{final_category}")
time.sleep(0.1)
db_manager.update_ip_stats_analysis(
ip=ip,
analyzed_metrics=generate_analyzed_metrics(),
category=final_category,
category_scores=generate_category_scores(),
last_analysis=datetime.now(tz=ZoneInfo('UTC'))
)
total_category_changes += 1
# Print summary
app_logger.info("\n" + "=" * 60)
app_logger.info("Test Data Generation Complete!")
app_logger.info("=" * 60)
app_logger.info(f"Total IPs created: {len(selected_ips)}")
app_logger.info(f"Total access logs: {total_logs}")
app_logger.info(f"Total attack detections: {total_attacks}")
app_logger.info(f"Total credential attempts: {total_credentials}")
app_logger.info(f"Total category changes: {total_category_changes}")
app_logger.info("=" * 60)
app_logger.info("\nYou can now view the dashboard with this test data.")
app_logger.info("The 'Behavior Timeline' will show category transitions for each IP.")
app_logger.info("Run: python server.py")
app_logger.info("=" * 60)
if __name__ == "__main__":
import sys
# Allow command-line arguments for customization
num_ips = int(sys.argv[1]) if len(sys.argv) > 1 else 5
logs_per_ip = int(sys.argv[2]) if len(sys.argv) > 2 else 15
credentials_per_ip = int(sys.argv[3]) if len(sys.argv) > 3 else 3
generate_fake_data(num_ips, logs_per_ip, credentials_per_ip)