From 2dd35234c05af68844d9d47db46175bac62fbaa3 Mon Sep 17 00:00:00 2001 From: Patrick Di Fazio Date: Sat, 17 Jan 2026 22:41:19 +0100 Subject: [PATCH 1/2] fixed dashboard alignment --- malicious_ips.txt | 1 + src/exports/malicious_ips.txt | 7 + src/templates/dashboard_template.py | 239 +++++++++++-------------- tests/test_insert_fake_ips.py | 259 ++++++++++++++++++++++++++++ 4 files changed, 371 insertions(+), 135 deletions(-) create mode 100644 malicious_ips.txt create mode 100644 src/exports/malicious_ips.txt create mode 100644 tests/test_insert_fake_ips.py diff --git a/malicious_ips.txt b/malicious_ips.txt new file mode 100644 index 0000000..7b9ad53 --- /dev/null +++ b/malicious_ips.txt @@ -0,0 +1 @@ +127.0.0.1 diff --git a/src/exports/malicious_ips.txt b/src/exports/malicious_ips.txt new file mode 100644 index 0000000..a82f110 --- /dev/null +++ b/src/exports/malicious_ips.txt @@ -0,0 +1,7 @@ +198.51.100.89 +203.0.113.45 +210.45.67.89 +182.91.102.45 +192.0.2.120 +205.32.180.65 +175.23.45.67 diff --git a/src/templates/dashboard_template.py b/src/templates/dashboard_template.py index 4c5a77a..8ee73a4 100644 --- a/src/templates/dashboard_template.py +++ b/src/templates/dashboard_template.py @@ -401,101 +401,47 @@ def generate_dashboard(stats: dict, timezone: str = 'UTC', dashboard_path: 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; @@ -504,28 +450,60 @@ def generate_dashboard(stats: dict, timezone: str = 'UTC', dashboard_path: 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; }} @@ -846,71 +824,62 @@ def generate_dashboard(stats: dict, timezone: str = 'UTC', dashboard_path: str = }} if (stats.category_history && stats.category_history.length > 0) {{ + html += '
'; html += '
'; - html += '
'; - html += '
Behavior Timeline
'; - - if (stats.list_on && Object.keys(stats.list_on).length > 0) {{ - html += '
'; - html += 'Listed on'; - - 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 += ``; - html += ''; - html += `${{source}}`; - html += ''; - }} else {{ - html += ''; - html += ''; - html += `${{source}}`; - html += ''; - }} - }}); - - html += '
'; - }} else if (stats.country_code || stats.asn) {{ - html += '
'; - html += 'Reputation'; - html += ''; - html += ''; - html += 'Clean'; - html += ''; - html += '
'; - }} - - html += '
'; - + // Timeline column + html += '
'; + html += '
Behavior Timeline
'; html += '
'; - 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 += '
'; html += `
`; html += '
'; - + if (change.old_category) {{ - const oldCategoryBadge = 'category-' + change.old_category.toLowerCase().replace('_', '-'); - html += `${{change.old_category}}`; - html += ''; + html += `${{change.old_category}}`; + html += ''; }} else {{ - html += 'Initial: '; + html += 'Initial:'; }} - - const newCategoryBadge = 'category-' + change.new_category.toLowerCase().replace('_', '-'); - html += `${{change.new_category}}`; - html += `
${{timestamp}}
`; + + html += `${{change.new_category}}`; + html += `
${{timestamp}}
`; html += '
'; html += '
'; }}); html += '
'; html += '
'; + + // Reputation column + html += '
'; + + if (stats.list_on && Object.keys(stats.list_on).length > 0) {{ + html += '
Listed On
'; + 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 += `${{source}}`; + }} else {{ + html += `${{source}}`; + }} + }}); + }} else if (stats.country_code || stats.asn) {{ + html += '
Reputation
'; + html += '✓ Clean'; + }} + + html += '
'; + html += '
'; + html += '
'; }} html += ''; diff --git a/tests/test_insert_fake_ips.py b/tests/test_insert_fake_ips.py new file mode 100644 index 0000000..6279b43 --- /dev/null +++ b/tests/test_insert_fake_ips.py @@ -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) From 59d99484e99fb40dba11fd9a2ea690aa1309d60b Mon Sep 17 00:00:00 2001 From: BlessedRebuS Date: Sat, 17 Jan 2026 22:43:42 +0100 Subject: [PATCH 2/2] fixed dashboard alignment --- malicious_ips.txt | 1 - src/exports/malicious_ips.txt | 7 ------- 2 files changed, 8 deletions(-) delete mode 100644 malicious_ips.txt delete mode 100644 src/exports/malicious_ips.txt diff --git a/malicious_ips.txt b/malicious_ips.txt deleted file mode 100644 index 7b9ad53..0000000 --- a/malicious_ips.txt +++ /dev/null @@ -1 +0,0 @@ -127.0.0.1 diff --git a/src/exports/malicious_ips.txt b/src/exports/malicious_ips.txt deleted file mode 100644 index a82f110..0000000 --- a/src/exports/malicious_ips.txt +++ /dev/null @@ -1,7 +0,0 @@ -198.51.100.89 -203.0.113.45 -210.45.67.89 -182.91.102.45 -192.0.2.120 -205.32.180.65 -175.23.45.67