Merge pull request #59 from BlessedRebuS/feat/blocklist-api
blocklist download api endpoint implementation
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -80,4 +80,7 @@ personal-values.yaml
|
|||||||
|
|
||||||
#exports dir (keeping .gitkeep so we have the dir)
|
#exports dir (keeping .gitkeep so we have the dir)
|
||||||
/exports/*
|
/exports/*
|
||||||
/src/exports/*
|
/src/exports/*
|
||||||
|
|
||||||
|
# tmux config
|
||||||
|
.tmux.conf
|
||||||
6
.tmux.conf
Normal file
6
.tmux.conf
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
splitw -v -p 10
|
||||||
|
neww -n worker
|
||||||
|
select-window -t 1
|
||||||
|
select-pane -t 0
|
||||||
|
send-keys -t 0 "nvim" C-m
|
||||||
|
send-keys -t 1 "docker compose watch" C-m
|
||||||
19
README.md
19
README.md
@@ -112,6 +112,8 @@ services:
|
|||||||
- TZ="Europe/Rome"
|
- TZ="Europe/Rome"
|
||||||
volumes:
|
volumes:
|
||||||
- ./config.yaml:/app/config.yaml:ro
|
- ./config.yaml:/app/config.yaml:ro
|
||||||
|
# bind mount for firewall exporters
|
||||||
|
- ./exports:/app/exports
|
||||||
- krawl-data:/app/data
|
- krawl-data:/app/data
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
@@ -208,6 +210,7 @@ Krawl uses a **configuration hierarchy** in which **environment variables take p
|
|||||||
| `KRAWL_DASHBOARD_SECRET_PATH` | Custom dashboard path | Auto-generated |
|
| `KRAWL_DASHBOARD_SECRET_PATH` | Custom dashboard path | Auto-generated |
|
||||||
| `KRAWL_PROBABILITY_ERROR_CODES` | Error response probability (0-100%) | `0` |
|
| `KRAWL_PROBABILITY_ERROR_CODES` | Error response probability (0-100%) | `0` |
|
||||||
| `KRAWL_DATABASE_PATH` | Database file location | `data/krawl.db` |
|
| `KRAWL_DATABASE_PATH` | Database file location | `data/krawl.db` |
|
||||||
|
| `KRAWL_EXPORTS_PATH` | Path where firewalls rule sets are exported | `exports` |
|
||||||
| `KRAWL_DATABASE_RETENTION_DAYS` | Days to retain data in database | `30` |
|
| `KRAWL_DATABASE_RETENTION_DAYS` | Days to retain data in database | `30` |
|
||||||
| `KRAWL_HTTP_RISKY_METHODS_THRESHOLD` | Threshold for risky HTTP methods detection | `0.1` |
|
| `KRAWL_HTTP_RISKY_METHODS_THRESHOLD` | Threshold for risky HTTP methods detection | `0.1` |
|
||||||
| `KRAWL_VIOLATED_ROBOTS_THRESHOLD` | Threshold for robots.txt violations | `0.1` |
|
| `KRAWL_VIOLATED_ROBOTS_THRESHOLD` | Threshold for robots.txt violations | `0.1` |
|
||||||
@@ -223,7 +226,7 @@ For example
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Set canary token
|
# Set canary token
|
||||||
export CONFIG_LOCATION="config.yaml"
|
export CONFIG_LOCATION="config.yaml"
|
||||||
export KRAWL_CANARY_TOKEN_URL="http://your-canary-token-url"
|
export KRAWL_CANARY_TOKEN_URL="http://your-canary-token-url"
|
||||||
|
|
||||||
# Set number of pages range (min,max format)
|
# Set number of pages range (min,max format)
|
||||||
@@ -256,7 +259,7 @@ You can use the [config.yaml](config.yaml) file for more advanced configurations
|
|||||||
Below is a complete overview of the Krawl honeypot’s capabilities
|
Below is a complete overview of the Krawl honeypot’s capabilities
|
||||||
|
|
||||||
## robots.txt
|
## robots.txt
|
||||||
The actual (juicy) robots.txt configuration [is the following](src/templates/html/robots.txt).
|
The actual (juicy) robots.txt configuration [is the following](src/templates/html/robots.txt).
|
||||||
|
|
||||||
## Honeypot pages
|
## Honeypot pages
|
||||||
Requests to common admin endpoints (`/admin/`, `/wp-admin/`, `/phpMyAdmin/`) return a fake login page. Any login attempt triggers a 1-second delay to simulate real processing and is fully logged in the dashboard (credentials, IP, headers, timing).
|
Requests to common admin endpoints (`/admin/`, `/wp-admin/`, `/phpMyAdmin/`) return a fake login page. Any login attempt triggers a 1-second delay to simulate real processing and is fully logged in the dashboard (credentials, IP, headers, timing).
|
||||||
@@ -278,11 +281,11 @@ The pages `/api/v1/users` and `/api/v2/secrets` show fake users and random secre
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
The pages `/credentials.txt` and `/passwords.txt` show fake users and random secrets
|
The pages `/credentials.txt` and `/passwords.txt` show fake users and random secrets
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Pages such as `/users`, `/search`, `/contact`, `/info`, `/input`, and `/feedback`, along with APIs like `/api/sql` and `/api/database`, are designed to lure attackers into performing attacks such as **SQL injection** or **XSS**.
|
Pages such as `/users`, `/search`, `/contact`, `/info`, `/input`, and `/feedback`, along with APIs like `/api/sql` and `/api/database`, are designed to lure attackers into performing attacks such as **SQL injection** or **XSS**.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -298,7 +301,7 @@ This optional token is triggered when a crawler fully traverses the webpage unti
|
|||||||
|
|
||||||
To enable this feature, set the canary token URL [using the environment variable](#configuration-via-environment-variables) `CANARY_TOKEN_URL`.
|
To enable this feature, set the canary token URL [using the environment variable](#configuration-via-environment-variables) `CANARY_TOKEN_URL`.
|
||||||
|
|
||||||
## Customizing the wordlist
|
## Customizing the wordlist
|
||||||
|
|
||||||
Edit `wordlists.json` to customize fake data for your use case
|
Edit `wordlists.json` to customize fake data for your use case
|
||||||
|
|
||||||
@@ -331,7 +334,7 @@ The dashboard shows:
|
|||||||
- Top IPs, paths, user-agents and GeoIP localization
|
- Top IPs, paths, user-agents and GeoIP localization
|
||||||
- Real-time monitoring
|
- Real-time monitoring
|
||||||
|
|
||||||
The attackers’ access to the honeypot endpoint and related suspicious activities (such as failed login attempts) are logged.
|
The attackers’ access to the honeypot endpoint and related suspicious activities (such as failed login attempts) are logged.
|
||||||
|
|
||||||
Krawl also implements a scoring system designed to distinguish between malicious and legitimate behavior on the website.
|
Krawl also implements a scoring system designed to distinguish between malicious and legitimate behavior on the website.
|
||||||
|
|
||||||
@@ -356,8 +359,8 @@ Contributions welcome! Please:
|
|||||||
|
|
||||||
## ⚠️ Disclaimer
|
## ⚠️ Disclaimer
|
||||||
|
|
||||||
**This is a deception/honeypot system.**
|
**This is a deception/honeypot system.**
|
||||||
Deploy in isolated environments and monitor carefully for security events.
|
Deploy in isolated environments and monitor carefully for security events.
|
||||||
Use responsibly and in compliance with applicable laws and regulations.
|
Use responsibly and in compliance with applicable laws and regulations.
|
||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ dashboard:
|
|||||||
# secret_path: super-secret-dashboard-path
|
# secret_path: super-secret-dashboard-path
|
||||||
secret_path: test
|
secret_path: test
|
||||||
|
|
||||||
|
exports:
|
||||||
|
path: "exports"
|
||||||
|
|
||||||
database:
|
database:
|
||||||
path: "data/krawl.db"
|
path: "data/krawl.db"
|
||||||
retention_days: 30
|
retention_days: 30
|
||||||
@@ -43,4 +46,4 @@ analyzer:
|
|||||||
crawl:
|
crawl:
|
||||||
infinite_pages_for_malicious: true
|
infinite_pages_for_malicious: true
|
||||||
max_pages_limit: 250
|
max_pages_limit: 250
|
||||||
ban_duration_seconds: 600
|
ban_duration_seconds: 600
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
---
|
---
|
||||||
|
# THIS IS FOR DEVELOPMENT PURPOSES
|
||||||
services:
|
services:
|
||||||
krawl:
|
krawl:
|
||||||
build:
|
build:
|
||||||
@@ -16,17 +17,13 @@ services:
|
|||||||
- ./config.yaml:/app/config.yaml:ro
|
- ./config.yaml:/app/config.yaml:ro
|
||||||
- ./logs:/app/logs
|
- ./logs:/app/logs
|
||||||
- ./exports:/app/exports
|
- ./exports:/app/exports
|
||||||
- data:/app/data
|
- ./data:/app/data
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
develop:
|
develop:
|
||||||
watch:
|
watch:
|
||||||
- path: ./Dockerfile
|
- path: ./Dockerfile
|
||||||
action: rebuild
|
action: rebuild
|
||||||
- path: ./src/
|
- path: ./src/
|
||||||
action: sync+restart
|
action: rebuild
|
||||||
target: /app/src
|
|
||||||
- path: ./docker-compose.yaml
|
- path: ./docker-compose.yaml
|
||||||
action: rebuild
|
action: rebuild
|
||||||
|
|
||||||
volumes:
|
|
||||||
data:
|
|
||||||
|
|||||||
50
docs/firewall-exporters.md
Normal file
50
docs/firewall-exporters.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# Firewall exporters documentation
|
||||||
|
|
||||||
|
Firewall export feature is implemented trough a strategy pattern with an abstract class and a series of subclasses that implement the specific export logic for each firewall specific system:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class FWType{
|
||||||
|
+getBanlist()
|
||||||
|
}
|
||||||
|
FWType <|-- Raw
|
||||||
|
class Raw{ }
|
||||||
|
FWType <|-- Iptables
|
||||||
|
class Iptables{ }
|
||||||
|
note for Iptables "implements the getBanlist method for iptables rules"
|
||||||
|
```
|
||||||
|
|
||||||
|
Rule sets are generated trough the `top_attacking_ips__export-malicious-ips` that writes down the files in the `exports_path` configuration path. Files are named after the specific firewall that they implement as `[firewall]_banlist.txt` except for raw file that is called `malicious_ips.txt` to support legacy
|
||||||
|
|
||||||
|
## Adding firewalls exporters
|
||||||
|
|
||||||
|
To add a firewall exporter create a new python class in `src/firewall` that implements `FWType` class
|
||||||
|
|
||||||
|
> example with `Yourfirewall` class in the `yourfirewall.py` file
|
||||||
|
```python
|
||||||
|
from typing_extensions import override
|
||||||
|
from firewall.fwtype import FWType
|
||||||
|
|
||||||
|
class Yourfirewall(FWType):
|
||||||
|
|
||||||
|
@override
|
||||||
|
def getBanlist(self, ips) -> str:
|
||||||
|
"""
|
||||||
|
Generate raw list of bad IP addresses.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ips: List of IP addresses to ban
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
String containing raw ips, one per line
|
||||||
|
"""
|
||||||
|
if not ips:
|
||||||
|
return ""
|
||||||
|
# Add here code implementation
|
||||||
|
```
|
||||||
|
|
||||||
|
Then add the following to the `src/server.py` and `src/tasks/top_attacking_ips.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
from firewall.yourfirewall import Yourfirewall
|
||||||
|
```
|
||||||
@@ -3,7 +3,7 @@ name: krawl-chart
|
|||||||
description: A Helm chart for Krawl honeypot server
|
description: A Helm chart for Krawl honeypot server
|
||||||
type: application
|
type: application
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
appVersion: 1.0.1
|
appVersion: 1.0.2
|
||||||
keywords:
|
keywords:
|
||||||
- honeypot
|
- honeypot
|
||||||
- security
|
- security
|
||||||
|
|||||||
@@ -37,9 +37,12 @@ class Config:
|
|||||||
infinite_pages_for_malicious: bool = True # Infinite pages for malicious crawlers
|
infinite_pages_for_malicious: bool = True # Infinite pages for malicious crawlers
|
||||||
ban_duration_seconds: int = 600 # Ban duration in seconds for IPs exceeding limits
|
ban_duration_seconds: int = 600 # Ban duration in seconds for IPs exceeding limits
|
||||||
|
|
||||||
|
# exporter settings
|
||||||
|
exports_path: str = "exports"
|
||||||
# Database settings
|
# Database settings
|
||||||
database_path: str = "data/krawl.db"
|
database_path: str = "data/krawl.db"
|
||||||
database_retention_days: int = 30
|
database_retention_days: int = 30
|
||||||
|
exports_path: str = "data/exports"
|
||||||
|
|
||||||
# Analyzer settings
|
# Analyzer settings
|
||||||
http_risky_methods_threshold: float = None
|
http_risky_methods_threshold: float = None
|
||||||
@@ -150,6 +153,7 @@ class Config:
|
|||||||
canary = data.get("canary", {})
|
canary = data.get("canary", {})
|
||||||
dashboard = data.get("dashboard", {})
|
dashboard = data.get("dashboard", {})
|
||||||
api = data.get("api", {})
|
api = data.get("api", {})
|
||||||
|
exports = data.get("exports", {})
|
||||||
database = data.get("database", {})
|
database = data.get("database", {})
|
||||||
behavior = data.get("behavior", {})
|
behavior = data.get("behavior", {})
|
||||||
analyzer = data.get("analyzer") or {}
|
analyzer = data.get("analyzer") or {}
|
||||||
@@ -185,6 +189,7 @@ class Config:
|
|||||||
canary_token_tries=canary.get("token_tries", 10),
|
canary_token_tries=canary.get("token_tries", 10),
|
||||||
dashboard_secret_path=dashboard_path,
|
dashboard_secret_path=dashboard_path,
|
||||||
probability_error_codes=behavior.get("probability_error_codes", 0),
|
probability_error_codes=behavior.get("probability_error_codes", 0),
|
||||||
|
exports_path=exports.get("path"),
|
||||||
database_path=database.get("path", "data/krawl.db"),
|
database_path=database.get("path", "data/krawl.db"),
|
||||||
database_retention_days=database.get("retention_days", 30),
|
database_retention_days=database.get("retention_days", 30),
|
||||||
http_risky_methods_threshold=analyzer.get(
|
http_risky_methods_threshold=analyzer.get(
|
||||||
|
|||||||
@@ -147,7 +147,9 @@ class DatabaseManager:
|
|||||||
migrations_run.append("region")
|
migrations_run.append("region")
|
||||||
|
|
||||||
if "region_name" not in columns:
|
if "region_name" not in columns:
|
||||||
cursor.execute("ALTER TABLE ip_stats ADD COLUMN region_name VARCHAR(100)")
|
cursor.execute(
|
||||||
|
"ALTER TABLE ip_stats ADD COLUMN region_name VARCHAR(100)"
|
||||||
|
)
|
||||||
migrations_run.append("region_name")
|
migrations_run.append("region_name")
|
||||||
|
|
||||||
if "timezone" not in columns:
|
if "timezone" not in columns:
|
||||||
|
|||||||
42
src/firewall/fwtype.py
Normal file
42
src/firewall/fwtype.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Dict, Type
|
||||||
|
|
||||||
|
|
||||||
|
class FWType(ABC):
|
||||||
|
"""Abstract base class for firewall types."""
|
||||||
|
|
||||||
|
# Registry to store child classes
|
||||||
|
_registry: Dict[str, Type["FWType"]] = {}
|
||||||
|
|
||||||
|
def __init_subclass__(cls, **kwargs):
|
||||||
|
"""Automatically register subclasses with their class name."""
|
||||||
|
super().__init_subclass__(**kwargs)
|
||||||
|
cls._registry[cls.__name__.lower()] = cls
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, fw_type: str, **kwargs) -> "FWType":
|
||||||
|
"""
|
||||||
|
Factory method to create instances of child classes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
fw_type: String name of the firewall type class to instantiate
|
||||||
|
**kwargs: Arguments to pass to the child class constructor
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Instance of the requested child class
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If fw_type is not registered
|
||||||
|
"""
|
||||||
|
fw_type = fw_type.lower()
|
||||||
|
if fw_type not in cls._registry:
|
||||||
|
available = ", ".join(cls._registry.keys())
|
||||||
|
raise ValueError(
|
||||||
|
f"Unknown firewall type: '{fw_type}'. Available: {available}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return cls._registry[fw_type](**kwargs)
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def getBanlist(self, ips):
|
||||||
|
"""Return the ruleset for the specific server"""
|
||||||
40
src/firewall/iptables.py
Normal file
40
src/firewall/iptables.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
from typing_extensions import override
|
||||||
|
from firewall.fwtype import FWType
|
||||||
|
|
||||||
|
|
||||||
|
class Iptables(FWType):
|
||||||
|
|
||||||
|
@override
|
||||||
|
def getBanlist(self, ips) -> str:
|
||||||
|
"""
|
||||||
|
Generate iptables ban rules from an array of IP addresses.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ips: List of IP addresses to ban
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
String containing iptables commands, one per line
|
||||||
|
"""
|
||||||
|
if not ips:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
rules = []
|
||||||
|
chain = "INPUT"
|
||||||
|
target = "DROP"
|
||||||
|
rules.append("#!/bin/bash")
|
||||||
|
rules.append("# iptables ban rules")
|
||||||
|
rules.append("")
|
||||||
|
|
||||||
|
for ip in ips:
|
||||||
|
|
||||||
|
ip = ip.strip()
|
||||||
|
|
||||||
|
# Build the iptables command
|
||||||
|
rule_parts = ["iptables", "-A", chain, "-s", ip]
|
||||||
|
|
||||||
|
# Add target
|
||||||
|
rule_parts.extend(["-j", target])
|
||||||
|
|
||||||
|
rules.append(" ".join(rule_parts))
|
||||||
|
|
||||||
|
return "\n".join(rules)
|
||||||
21
src/firewall/raw.py
Normal file
21
src/firewall/raw.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from typing_extensions import override
|
||||||
|
from firewall.fwtype import FWType
|
||||||
|
|
||||||
|
|
||||||
|
class Raw(FWType):
|
||||||
|
|
||||||
|
@override
|
||||||
|
def getBanlist(self, ips) -> str:
|
||||||
|
"""
|
||||||
|
Generate raw list of bad IP addresses.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ips: List of IP addresses to ban
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
String containing raw ips, one per line
|
||||||
|
"""
|
||||||
|
if not ips:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
return "\n".join(ips)
|
||||||
@@ -41,7 +41,9 @@ def fetch_ip_geolocation(ip_address: str) -> Optional[Dict[str, Any]]:
|
|||||||
|
|
||||||
# Check if the API call was successful
|
# Check if the API call was successful
|
||||||
if data.get("status") != "success":
|
if data.get("status") != "success":
|
||||||
app_logger.warning(f"IP lookup failed for {ip_address}: {data.get('message')}")
|
app_logger.warning(
|
||||||
|
f"IP lookup failed for {ip_address}: {data.get('message')}"
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Cache the result
|
# Cache the result
|
||||||
@@ -113,7 +115,7 @@ def fetch_blocklist_data(ip_address: str) -> Optional[Dict[str, Any]]:
|
|||||||
# Get the most recent result (first in list, sorted by record_added)
|
# Get the most recent result (first in list, sorted by record_added)
|
||||||
most_recent = results[0]
|
most_recent = results[0]
|
||||||
list_on = most_recent.get("list_on", {})
|
list_on = most_recent.get("list_on", {})
|
||||||
|
|
||||||
app_logger.debug(f"Fetched blocklist data for {ip_address}")
|
app_logger.debug(f"Fetched blocklist data for {ip_address}")
|
||||||
return list_on
|
return list_on
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
|
|||||||
148
src/handler.py
148
src/handler.py
@@ -7,8 +7,17 @@ from datetime import datetime
|
|||||||
from http.server import BaseHTTPRequestHandler
|
from http.server import BaseHTTPRequestHandler
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
from urllib.parse import urlparse, parse_qs
|
from urllib.parse import urlparse, parse_qs
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
from database import get_database
|
||||||
|
from config import Config, get_config
|
||||||
|
|
||||||
|
# imports for the __init_subclass__ method, do not remove pls
|
||||||
|
from firewall.fwtype import FWType
|
||||||
|
from firewall.iptables import Iptables
|
||||||
|
from firewall.raw import Raw
|
||||||
|
|
||||||
from config import Config
|
|
||||||
from tracker import AccessTracker
|
from tracker import AccessTracker
|
||||||
from analyzer import Analyzer
|
from analyzer import Analyzer
|
||||||
from templates import html_templates
|
from templates import html_templates
|
||||||
@@ -26,6 +35,9 @@ from wordlists import get_wordlists
|
|||||||
from sql_errors import generate_sql_error_response, get_sql_response_with_data
|
from sql_errors import generate_sql_error_response, get_sql_response_with_data
|
||||||
from xss_detector import detect_xss_pattern, generate_xss_response
|
from xss_detector import detect_xss_pattern, generate_xss_response
|
||||||
from server_errors import generate_server_error
|
from server_errors import generate_server_error
|
||||||
|
from models import AccessLog
|
||||||
|
from ip_utils import is_valid_public_ip
|
||||||
|
from sqlalchemy import distinct
|
||||||
|
|
||||||
|
|
||||||
class Handler(BaseHTTPRequestHandler):
|
class Handler(BaseHTTPRequestHandler):
|
||||||
@@ -58,10 +70,6 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
# Fallback to direct connection IP
|
# Fallback to direct connection IP
|
||||||
return self.client_address[0]
|
return self.client_address[0]
|
||||||
|
|
||||||
def _get_user_agent(self) -> str:
|
|
||||||
"""Extract user agent from request"""
|
|
||||||
return self.headers.get("User-Agent", "")
|
|
||||||
|
|
||||||
def _get_category_by_ip(self, client_ip: str) -> str:
|
def _get_category_by_ip(self, client_ip: str) -> str:
|
||||||
"""Get the category of an IP from the database"""
|
"""Get the category of an IP from the database"""
|
||||||
return self.tracker.get_category_by_ip(client_ip)
|
return self.tracker.get_category_by_ip(client_ip)
|
||||||
@@ -92,11 +100,6 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
error_codes = [400, 401, 403, 404, 500, 502, 503]
|
error_codes = [400, 401, 403, 404, 500, 502, 503]
|
||||||
return random.choice(error_codes)
|
return random.choice(error_codes)
|
||||||
|
|
||||||
def _parse_query_string(self) -> str:
|
|
||||||
"""Extract query string from the request path"""
|
|
||||||
parsed = urlparse(self.path)
|
|
||||||
return parsed.query
|
|
||||||
|
|
||||||
def _handle_sql_endpoint(self, path: str) -> bool:
|
def _handle_sql_endpoint(self, path: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Handle SQL injection honeypot endpoints.
|
Handle SQL injection honeypot endpoints.
|
||||||
@@ -111,21 +114,20 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Get query parameters
|
# Get query parameters
|
||||||
query_string = self._parse_query_string()
|
|
||||||
|
|
||||||
# Log SQL injection attempt
|
# Log SQL injection attempt
|
||||||
client_ip = self._get_client_ip()
|
client_ip = self._get_client_ip()
|
||||||
user_agent = self._get_user_agent()
|
user_agent = self.headers.get("User-Agent", "")
|
||||||
|
|
||||||
# Always check for SQL injection patterns
|
# Always check for SQL injection patterns
|
||||||
error_msg, content_type, status_code = generate_sql_error_response(
|
error_msg, content_type, status_code = generate_sql_error_response(
|
||||||
query_string or ""
|
request_query or ""
|
||||||
)
|
)
|
||||||
|
|
||||||
if error_msg:
|
if error_msg:
|
||||||
# SQL injection detected - log and return error
|
# SQL injection detected - log and return error
|
||||||
self.access_logger.warning(
|
self.access_logger.warning(
|
||||||
f"[SQL INJECTION DETECTED] {client_ip} - {base_path} - Query: {query_string[:100] if query_string else 'empty'}"
|
f"[SQL INJECTION DETECTED] {client_ip} - {base_path} - Query: {request_query[:100] if request_query else 'empty'}"
|
||||||
)
|
)
|
||||||
self.send_response(status_code)
|
self.send_response(status_code)
|
||||||
self.send_header("Content-type", content_type)
|
self.send_header("Content-type", content_type)
|
||||||
@@ -134,13 +136,13 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
else:
|
else:
|
||||||
# No injection detected - return fake data
|
# No injection detected - return fake data
|
||||||
self.access_logger.info(
|
self.access_logger.info(
|
||||||
f"[SQL ENDPOINT] {client_ip} - {base_path} - Query: {query_string[:100] if query_string else 'empty'}"
|
f"[SQL ENDPOINT] {client_ip} - {base_path} - Query: {request_query[:100] if request_query else 'empty'}"
|
||||||
)
|
)
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header("Content-type", "application/json")
|
self.send_header("Content-type", "application/json")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
response_data = get_sql_response_with_data(
|
response_data = get_sql_response_with_data(
|
||||||
base_path, query_string or ""
|
base_path, request_query or ""
|
||||||
)
|
)
|
||||||
self.wfile.write(response_data.encode())
|
self.wfile.write(response_data.encode())
|
||||||
|
|
||||||
@@ -239,11 +241,9 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
def do_POST(self):
|
def do_POST(self):
|
||||||
"""Handle POST requests (mainly login attempts)"""
|
"""Handle POST requests (mainly login attempts)"""
|
||||||
client_ip = self._get_client_ip()
|
client_ip = self._get_client_ip()
|
||||||
user_agent = self._get_user_agent()
|
user_agent = self.headers.get("User-Agent", "")
|
||||||
post_data = ""
|
post_data = ""
|
||||||
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
base_path = urlparse(self.path).path
|
base_path = urlparse(self.path).path
|
||||||
|
|
||||||
if base_path in ["/api/search", "/api/sql", "/api/database"]:
|
if base_path in ["/api/search", "/api/sql", "/api/database"]:
|
||||||
@@ -293,7 +293,6 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
for pair in post_data.split("&"):
|
for pair in post_data.split("&"):
|
||||||
if "=" in pair:
|
if "=" in pair:
|
||||||
key, value = pair.split("=", 1)
|
key, value = pair.split("=", 1)
|
||||||
from urllib.parse import unquote_plus
|
|
||||||
|
|
||||||
parsed_data[unquote_plus(key)] = unquote_plus(value)
|
parsed_data[unquote_plus(key)] = unquote_plus(value)
|
||||||
|
|
||||||
@@ -486,18 +485,30 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
"""Responds to webpage requests"""
|
"""Responds to webpage requests"""
|
||||||
|
|
||||||
client_ip = self._get_client_ip()
|
client_ip = self._get_client_ip()
|
||||||
|
|
||||||
|
# respond with HTTP error code if client is banned
|
||||||
if self.tracker.is_banned_ip(client_ip):
|
if self.tracker.is_banned_ip(client_ip):
|
||||||
self.send_response(500)
|
self.send_response(500)
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
return
|
return
|
||||||
user_agent = self._get_user_agent()
|
|
||||||
|
# get request data
|
||||||
|
user_agent = self.headers.get("User-Agent", "")
|
||||||
|
request_path = urlparse(self.path).path
|
||||||
|
self.app_logger.info(f"request_query: {request_path}")
|
||||||
|
query_params = parse_qs(urlparse(self.path).query)
|
||||||
|
self.app_logger.info(f"query_params: {query_params}")
|
||||||
|
|
||||||
|
# get database reference
|
||||||
|
db = get_database()
|
||||||
|
session = db.session
|
||||||
|
|
||||||
# Handle static files for dashboard
|
# Handle static files for dashboard
|
||||||
if self.config.dashboard_secret_path and self.path.startswith(
|
if self.config.dashboard_secret_path and self.path.startswith(
|
||||||
f"{self.config.dashboard_secret_path}/static/"
|
f"{self.config.dashboard_secret_path}/static/"
|
||||||
):
|
):
|
||||||
import os
|
|
||||||
|
|
||||||
file_path = self.path.replace(
|
file_path = self.path.replace(
|
||||||
f"{self.config.dashboard_secret_path}/static/", ""
|
f"{self.config.dashboard_secret_path}/static/", ""
|
||||||
@@ -543,8 +554,11 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self.end_headers()
|
self.end_headers()
|
||||||
try:
|
try:
|
||||||
stats = self.tracker.get_stats()
|
stats = self.tracker.get_stats()
|
||||||
dashboard_path = self.config.dashboard_secret_path
|
self.wfile.write(
|
||||||
self.wfile.write(generate_dashboard(stats, dashboard_path).encode())
|
generate_dashboard(
|
||||||
|
stats, self.config.dashboard_secret_path
|
||||||
|
).encode()
|
||||||
|
)
|
||||||
except BrokenPipeError:
|
except BrokenPipeError:
|
||||||
pass
|
pass
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -566,10 +580,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self.send_header("Expires", "0")
|
self.send_header("Expires", "0")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
try:
|
try:
|
||||||
from database import get_database
|
|
||||||
import json
|
|
||||||
|
|
||||||
db = get_database()
|
|
||||||
ip_stats_list = db.get_ip_stats(limit=500)
|
ip_stats_list = db.get_ip_stats(limit=500)
|
||||||
self.wfile.write(json.dumps({"ips": ip_stats_list}).encode())
|
self.wfile.write(json.dumps({"ips": ip_stats_list}).encode())
|
||||||
except BrokenPipeError:
|
except BrokenPipeError:
|
||||||
@@ -593,15 +604,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self.send_header("Expires", "0")
|
self.send_header("Expires", "0")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
try:
|
try:
|
||||||
from database import get_database
|
|
||||||
import json
|
|
||||||
from urllib.parse import urlparse, parse_qs
|
|
||||||
|
|
||||||
db = get_database()
|
|
||||||
|
|
||||||
# Parse query parameters
|
|
||||||
parsed_url = urlparse(self.path)
|
|
||||||
query_params = parse_qs(parsed_url.query)
|
|
||||||
page = int(query_params.get("page", ["1"])[0])
|
page = int(query_params.get("page", ["1"])[0])
|
||||||
page_size = int(query_params.get("page_size", ["25"])[0])
|
page_size = int(query_params.get("page_size", ["25"])[0])
|
||||||
sort_by = query_params.get("sort_by", ["total_requests"])[0]
|
sort_by = query_params.get("sort_by", ["total_requests"])[0]
|
||||||
@@ -639,11 +642,6 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self.send_header("Expires", "0")
|
self.send_header("Expires", "0")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
try:
|
try:
|
||||||
from database import get_database
|
|
||||||
import json
|
|
||||||
from urllib.parse import urlparse, parse_qs
|
|
||||||
|
|
||||||
db = get_database()
|
|
||||||
|
|
||||||
# Parse query parameters
|
# Parse query parameters
|
||||||
parsed_url = urlparse(self.path)
|
parsed_url = urlparse(self.path)
|
||||||
@@ -689,10 +687,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self.send_header("Expires", "0")
|
self.send_header("Expires", "0")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
try:
|
try:
|
||||||
from database import get_database
|
|
||||||
import json
|
|
||||||
|
|
||||||
db = get_database()
|
|
||||||
ip_stats = db.get_ip_stats_by_ip(ip_address)
|
ip_stats = db.get_ip_stats_by_ip(ip_address)
|
||||||
if ip_stats:
|
if ip_stats:
|
||||||
self.wfile.write(json.dumps(ip_stats).encode())
|
self.wfile.write(json.dumps(ip_stats).encode())
|
||||||
@@ -719,11 +714,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self.send_header("Expires", "0")
|
self.send_header("Expires", "0")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
try:
|
try:
|
||||||
from database import get_database
|
|
||||||
import json
|
|
||||||
from urllib.parse import urlparse, parse_qs
|
|
||||||
|
|
||||||
db = get_database()
|
|
||||||
parsed_url = urlparse(self.path)
|
parsed_url = urlparse(self.path)
|
||||||
query_params = parse_qs(parsed_url.query)
|
query_params = parse_qs(parsed_url.query)
|
||||||
page = int(query_params.get("page", ["1"])[0])
|
page = int(query_params.get("page", ["1"])[0])
|
||||||
@@ -762,11 +753,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self.send_header("Expires", "0")
|
self.send_header("Expires", "0")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
try:
|
try:
|
||||||
from database import get_database
|
|
||||||
import json
|
|
||||||
from urllib.parse import urlparse, parse_qs
|
|
||||||
|
|
||||||
db = get_database()
|
|
||||||
parsed_url = urlparse(self.path)
|
parsed_url = urlparse(self.path)
|
||||||
query_params = parse_qs(parsed_url.query)
|
query_params = parse_qs(parsed_url.query)
|
||||||
page = int(query_params.get("page", ["1"])[0])
|
page = int(query_params.get("page", ["1"])[0])
|
||||||
@@ -805,11 +792,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self.send_header("Expires", "0")
|
self.send_header("Expires", "0")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
try:
|
try:
|
||||||
from database import get_database
|
|
||||||
import json
|
|
||||||
from urllib.parse import urlparse, parse_qs
|
|
||||||
|
|
||||||
db = get_database()
|
|
||||||
parsed_url = urlparse(self.path)
|
parsed_url = urlparse(self.path)
|
||||||
query_params = parse_qs(parsed_url.query)
|
query_params = parse_qs(parsed_url.query)
|
||||||
page = int(query_params.get("page", ["1"])[0])
|
page = int(query_params.get("page", ["1"])[0])
|
||||||
@@ -848,11 +831,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self.send_header("Expires", "0")
|
self.send_header("Expires", "0")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
try:
|
try:
|
||||||
from database import get_database
|
|
||||||
import json
|
|
||||||
from urllib.parse import urlparse, parse_qs
|
|
||||||
|
|
||||||
db = get_database()
|
|
||||||
parsed_url = urlparse(self.path)
|
parsed_url = urlparse(self.path)
|
||||||
query_params = parse_qs(parsed_url.query)
|
query_params = parse_qs(parsed_url.query)
|
||||||
page = int(query_params.get("page", ["1"])[0])
|
page = int(query_params.get("page", ["1"])[0])
|
||||||
@@ -891,11 +870,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self.send_header("Expires", "0")
|
self.send_header("Expires", "0")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
try:
|
try:
|
||||||
from database import get_database
|
|
||||||
import json
|
|
||||||
from urllib.parse import urlparse, parse_qs
|
|
||||||
|
|
||||||
db = get_database()
|
|
||||||
parsed_url = urlparse(self.path)
|
parsed_url = urlparse(self.path)
|
||||||
query_params = parse_qs(parsed_url.query)
|
query_params = parse_qs(parsed_url.query)
|
||||||
page = int(query_params.get("page", ["1"])[0])
|
page = int(query_params.get("page", ["1"])[0])
|
||||||
@@ -934,11 +909,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self.send_header("Expires", "0")
|
self.send_header("Expires", "0")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
try:
|
try:
|
||||||
from database import get_database
|
|
||||||
import json
|
|
||||||
from urllib.parse import urlparse, parse_qs
|
|
||||||
|
|
||||||
db = get_database()
|
|
||||||
parsed_url = urlparse(self.path)
|
parsed_url = urlparse(self.path)
|
||||||
query_params = parse_qs(parsed_url.query)
|
query_params = parse_qs(parsed_url.query)
|
||||||
page = int(query_params.get("page", ["1"])[0])
|
page = int(query_params.get("page", ["1"])[0])
|
||||||
@@ -963,13 +934,54 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self.wfile.write(json.dumps({"error": str(e)}).encode())
|
self.wfile.write(json.dumps({"error": str(e)}).encode())
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# API endpoint for downloading malicious IPs blocklist file
|
||||||
|
if (
|
||||||
|
self.config.dashboard_secret_path
|
||||||
|
and request_path == f"{self.config.dashboard_secret_path}/api/get_banlist"
|
||||||
|
):
|
||||||
|
|
||||||
|
# get fwtype from request params
|
||||||
|
fwtype = query_params.get("fwtype", ["iptables"])[0]
|
||||||
|
filename = f"{fwtype}_banlist.txt"
|
||||||
|
if fwtype == "raw":
|
||||||
|
filename = f"malicious_ips.txt"
|
||||||
|
|
||||||
|
file_path = os.path.join(self.config.exports_path, f"{filename}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
with open(file_path, "rb") as f:
|
||||||
|
content = f.read()
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-type", "text/plain")
|
||||||
|
self.send_header(
|
||||||
|
"Content-Disposition",
|
||||||
|
f'attachment; filename="{filename}"',
|
||||||
|
)
|
||||||
|
self.send_header("Content-Length", str(len(content)))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(content)
|
||||||
|
else:
|
||||||
|
self.send_response(404)
|
||||||
|
self.send_header("Content-type", "text/plain")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(b"File not found")
|
||||||
|
except BrokenPipeError:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
self.app_logger.error(f"Error serving malicious IPs file: {e}")
|
||||||
|
self.send_response(500)
|
||||||
|
self.send_header("Content-type", "text/plain")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(b"Internal server error")
|
||||||
|
return
|
||||||
|
|
||||||
# API endpoint for downloading malicious IPs file
|
# API endpoint for downloading malicious IPs file
|
||||||
if (
|
if (
|
||||||
self.config.dashboard_secret_path
|
self.config.dashboard_secret_path
|
||||||
and self.path
|
and self.path
|
||||||
== f"{self.config.dashboard_secret_path}/api/download/malicious_ips.txt"
|
== f"{self.config.dashboard_secret_path}/api/download/malicious_ips.txt"
|
||||||
):
|
):
|
||||||
import os
|
|
||||||
|
|
||||||
file_path = os.path.join(
|
file_path = os.path.join(
|
||||||
os.path.dirname(__file__), "exports", "malicious_ips.txt"
|
os.path.dirname(__file__), "exports", "malicious_ips.txt"
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ def main():
|
|||||||
try:
|
try:
|
||||||
# Fetch geolocation data using ip-api.com
|
# Fetch geolocation data using ip-api.com
|
||||||
geoloc_data = extract_geolocation_from_ip(ip)
|
geoloc_data = extract_geolocation_from_ip(ip)
|
||||||
|
|
||||||
# Fetch blocklist data from lcrawl API
|
# Fetch blocklist data from lcrawl API
|
||||||
blocklist_data = fetch_blocklist_data(ip)
|
blocklist_data = fetch_blocklist_data(ip)
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ def main():
|
|||||||
list_on = blocklist_data
|
list_on = blocklist_data
|
||||||
else:
|
else:
|
||||||
list_on = {}
|
list_on = {}
|
||||||
|
|
||||||
# Add flags to list_on
|
# Add flags to list_on
|
||||||
list_on["is_proxy"] = is_proxy
|
list_on["is_proxy"] = is_proxy
|
||||||
list_on["is_hosting"] = is_hosting
|
list_on["is_hosting"] = is_hosting
|
||||||
@@ -69,7 +69,9 @@ def main():
|
|||||||
sanitized_city = sanitize_for_storage(city, 100) if city else None
|
sanitized_city = sanitize_for_storage(city, 100) if city else None
|
||||||
sanitized_timezone = sanitize_for_storage(timezone, 50)
|
sanitized_timezone = sanitize_for_storage(timezone, 50)
|
||||||
sanitized_isp = sanitize_for_storage(isp, 100)
|
sanitized_isp = sanitize_for_storage(isp, 100)
|
||||||
sanitized_reverse = sanitize_for_storage(reverse, 255) if reverse else None
|
sanitized_reverse = (
|
||||||
|
sanitize_for_storage(reverse, 255) if reverse else None
|
||||||
|
)
|
||||||
sanitized_list_on = sanitize_dict(list_on, 100000)
|
sanitized_list_on = sanitize_dict(list_on, 100000)
|
||||||
|
|
||||||
db_manager.update_ip_rep_infos(
|
db_manager.update_ip_rep_infos(
|
||||||
|
|||||||
@@ -4,9 +4,14 @@ import os
|
|||||||
from logger import get_app_logger
|
from logger import get_app_logger
|
||||||
from database import get_database
|
from database import get_database
|
||||||
from config import get_config
|
from config import get_config
|
||||||
from models import IpStats
|
from models import IpStats, AccessLog
|
||||||
from ip_utils import is_valid_public_ip
|
from ip_utils import is_valid_public_ip
|
||||||
|
from sqlalchemy import distinct
|
||||||
|
from firewall.fwtype import FWType
|
||||||
|
from firewall.iptables import Iptables
|
||||||
|
from firewall.raw import Raw
|
||||||
|
|
||||||
|
config = get_config()
|
||||||
app_logger = get_app_logger()
|
app_logger = get_app_logger()
|
||||||
|
|
||||||
# ----------------------
|
# ----------------------
|
||||||
@@ -20,7 +25,7 @@ TASK_CONFIG = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
EXPORTS_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "exports")
|
EXPORTS_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "exports")
|
||||||
OUTPUT_FILE = os.path.join(EXPORTS_DIR, "malicious_ips.txt")
|
EXPORTS_DIR = config.exports_path
|
||||||
|
|
||||||
|
|
||||||
# ----------------------
|
# ----------------------
|
||||||
@@ -48,7 +53,6 @@ def main():
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Filter out local/private IPs and the server's own IP
|
# Filter out local/private IPs and the server's own IP
|
||||||
config = get_config()
|
|
||||||
server_ip = config.get_server_ip()
|
server_ip = config.get_server_ip()
|
||||||
|
|
||||||
public_ips = [
|
public_ips = [
|
||||||
@@ -61,14 +65,24 @@ def main():
|
|||||||
os.makedirs(EXPORTS_DIR, exist_ok=True)
|
os.makedirs(EXPORTS_DIR, exist_ok=True)
|
||||||
|
|
||||||
# Write IPs to file (one per line)
|
# Write IPs to file (one per line)
|
||||||
with open(OUTPUT_FILE, "w") as f:
|
for fwname in FWType._registry:
|
||||||
for ip in public_ips:
|
|
||||||
f.write(f"{ip}\n")
|
|
||||||
|
|
||||||
app_logger.info(
|
# get banlist for specific ip
|
||||||
f"[Background Task] {task_name} exported {len(public_ips)} attacker IPs "
|
fw = FWType.create(fwname)
|
||||||
f"(filtered {len(attackers) - len(public_ips)} local/private IPs) to {OUTPUT_FILE}"
|
banlist = fw.getBanlist(public_ips)
|
||||||
)
|
|
||||||
|
output_file = os.path.join(EXPORTS_DIR, f"{fwname}_banlist.txt")
|
||||||
|
|
||||||
|
if fwname == "raw":
|
||||||
|
output_file = os.path.join(EXPORTS_DIR, f"malicious_ips.txt")
|
||||||
|
|
||||||
|
with open(output_file, "w") as f:
|
||||||
|
f.write(f"{banlist}\n")
|
||||||
|
|
||||||
|
app_logger.info(
|
||||||
|
f"[Background Task] {task_name} exported {len(public_ips)} in {fwname} public IPs"
|
||||||
|
f"(filtered {len(attackers) - len(public_ips)} local/private IPs) to {output_file}"
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
app_logger.error(f"[Background Task] {task_name} failed: {e}")
|
app_logger.error(f"[Background Task] {task_name} failed: {e}")
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import html
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
|
# imports for the __init_subclass__ method, do not remove pls
|
||||||
|
from firewall import fwtype
|
||||||
|
|
||||||
|
|
||||||
def _escape(value) -> str:
|
def _escape(value) -> str:
|
||||||
"""Escape HTML special characters to prevent XSS attacks."""
|
"""Escape HTML special characters to prevent XSS attacks."""
|
||||||
@@ -47,7 +50,9 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
|
|||||||
|
|
||||||
# Generate suspicious accesses rows with clickable IPs
|
# Generate suspicious accesses rows with clickable IPs
|
||||||
suspicious_rows = (
|
suspicious_rows = (
|
||||||
"\n".join([f"""<tr class="ip-row" data-ip="{_escape(log["ip"])}">
|
"\n".join(
|
||||||
|
[
|
||||||
|
f"""<tr class="ip-row" data-ip="{_escape(log["ip"])}">
|
||||||
<td class="ip-clickable">{_escape(log["ip"])}</td>
|
<td class="ip-clickable">{_escape(log["ip"])}</td>
|
||||||
<td>{_escape(log["path"])}</td>
|
<td>{_escape(log["path"])}</td>
|
||||||
<td style="word-break: break-all;">{_escape(log["user_agent"][:60])}</td>
|
<td style="word-break: break-all;">{_escape(log["user_agent"][:60])}</td>
|
||||||
@@ -59,7 +64,10 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
|
|||||||
<div class="loading">Loading stats...</div>
|
<div class="loading">Loading stats...</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>""" for log in stats["recent_suspicious"][-10:]])
|
</tr>"""
|
||||||
|
for log in stats["recent_suspicious"][-10:]
|
||||||
|
]
|
||||||
|
)
|
||||||
or '<tr><td colspan="4" style="text-align:center;">No suspicious activity detected</td></tr>'
|
or '<tr><td colspan="4" style="text-align:center;">No suspicious activity detected</td></tr>'
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -137,6 +145,68 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
|
|||||||
.download-btn:active {{
|
.download-btn:active {{
|
||||||
background: #1f7a2f;
|
background: #1f7a2f;
|
||||||
}}
|
}}
|
||||||
|
.banlist-dropdown {{
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
}}
|
||||||
|
.banlist-dropdown-btn {{
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
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;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}}
|
||||||
|
.banlist-dropdown-btn:hover {{
|
||||||
|
background: #2ea043;
|
||||||
|
}}
|
||||||
|
.banlist-dropdown-menu {{
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
background-color: #161b22;
|
||||||
|
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.3);
|
||||||
|
z-index: 1;
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-top: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}}
|
||||||
|
.banlist-dropdown-menu.show {{
|
||||||
|
display: block;
|
||||||
|
}}
|
||||||
|
.banlist-dropdown-menu a {{
|
||||||
|
color: #c9d1d9;
|
||||||
|
padding: 6px 12px;
|
||||||
|
text-decoration: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
font-size: 12px;
|
||||||
|
}}
|
||||||
|
.banlist-dropdown-menu a:hover {{
|
||||||
|
background-color: #1c2128;
|
||||||
|
color: #58a6ff;
|
||||||
|
}}
|
||||||
|
.banlist-dropdown-menu a.disabled {{
|
||||||
|
color: #6e7681;
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
}}
|
||||||
|
.banlist-icon {{
|
||||||
|
font-size: 14px;
|
||||||
|
}}
|
||||||
.stats-grid {{
|
.stats-grid {{
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
@@ -978,9 +1048,17 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
|
|||||||
<span class="github-logo-text">BlessedRebuS/Krawl</span>
|
<span class="github-logo-text">BlessedRebuS/Krawl</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="download-section">
|
<div class="download-section">
|
||||||
<a href="{dashboard_path}/api/download/malicious_ips.txt" class="download-btn" download>
|
<div class="banlist-dropdown">
|
||||||
Export Malicious IPs
|
<button class="banlist-dropdown-btn" onclick="toggleBanlistDropdown()">Export IPs Banlist</button>
|
||||||
</a>
|
<div id="banlistDropdown" class="banlist-dropdown-menu">
|
||||||
|
<a href="javascript:void(0)" onclick="downloadBanlist('raw')">
|
||||||
|
<span>Raw IPs</span>
|
||||||
|
</a>
|
||||||
|
<a href="javascript:void(0)" onclick="downloadBanlist('iptables')">
|
||||||
|
<span>IPTables Rules</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h1>Krawl Dashboard</h1>
|
<h1>Krawl Dashboard</h1>
|
||||||
|
|
||||||
@@ -1269,6 +1347,43 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
|
|||||||
<script>
|
<script>
|
||||||
const DASHBOARD_PATH = '{dashboard_path}';
|
const DASHBOARD_PATH = '{dashboard_path}';
|
||||||
|
|
||||||
|
// Dropdown menu functions
|
||||||
|
function toggleBanlistDropdown() {{
|
||||||
|
const dropdown = document.getElementById('banlistDropdown');
|
||||||
|
dropdown.classList.toggle('show');
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Close dropdown when clicking outside
|
||||||
|
document.addEventListener('click', function(event) {{
|
||||||
|
const dropdown = document.querySelector('.banlist-dropdown');
|
||||||
|
if (!dropdown.contains(event.target)) {{
|
||||||
|
const menu = document.getElementById('banlistDropdown');
|
||||||
|
menu.classList.remove('show');
|
||||||
|
}}
|
||||||
|
}});
|
||||||
|
|
||||||
|
// Download banlist function
|
||||||
|
function downloadBanlist(fwtype) {{
|
||||||
|
const url = DASHBOARD_PATH + '/api/get_banlist?fwtype=' + encodeURIComponent(fwtype);
|
||||||
|
|
||||||
|
// Create a temporary link and trigger download
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
|
||||||
|
// Set filename based on type
|
||||||
|
const filename = fwtype === 'raw' ? 'banlist_raw.txt' : 'banlist_iptables.sh';
|
||||||
|
link.setAttribute('download', filename);
|
||||||
|
|
||||||
|
// Append to body, click, and remove
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
|
||||||
|
// Close dropdown after download
|
||||||
|
const menu = document.getElementById('banlistDropdown');
|
||||||
|
menu.classList.remove('show');
|
||||||
|
}}
|
||||||
|
|
||||||
function formatTimestamp(isoTimestamp) {{
|
function formatTimestamp(isoTimestamp) {{
|
||||||
if (!isoTimestamp) return 'N/A';
|
if (!isoTimestamp) return 'N/A';
|
||||||
try {{
|
try {{
|
||||||
@@ -1457,7 +1572,7 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
|
|||||||
if (stats.category_history && stats.category_history.length > 0) {{
|
if (stats.category_history && stats.category_history.length > 0) {{
|
||||||
html += '<div class="timeline-section">';
|
html += '<div class="timeline-section">';
|
||||||
html += '<div class="timeline-container">';
|
html += '<div class="timeline-container">';
|
||||||
|
|
||||||
// Timeline column
|
// Timeline column
|
||||||
html += '<div class="timeline-column">';
|
html += '<div class="timeline-column">';
|
||||||
html += '<div class="timeline-header">Behavior Timeline</div>';
|
html += '<div class="timeline-header">Behavior Timeline</div>';
|
||||||
@@ -1468,16 +1583,16 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
|
|||||||
const timestamp = formatTimestamp(change.timestamp);
|
const timestamp = formatTimestamp(change.timestamp);
|
||||||
const oldClass = change.old_category ? 'category-' + change.old_category.toLowerCase().replace('_', '-') : '';
|
const oldClass = change.old_category ? 'category-' + change.old_category.toLowerCase().replace('_', '-') : '';
|
||||||
const newClass = 'category-' + categoryClass;
|
const newClass = 'category-' + categoryClass;
|
||||||
|
|
||||||
html += '<div class="timeline-item">';
|
html += '<div class="timeline-item">';
|
||||||
html += `<div class="timeline-marker ${{categoryClass}}"></div>`;
|
html += `<div class="timeline-marker ${{categoryClass}}"></div>`;
|
||||||
html += '<div class="timeline-content">';
|
html += '<div class="timeline-content">';
|
||||||
|
|
||||||
if (change.old_category) {{
|
if (change.old_category) {{
|
||||||
html += `<span class="category-badge ${{oldClass}}">${{change.old_category}}</span>`;
|
html += `<span class="category-badge ${{oldClass}}">${{change.old_category}}</span>`;
|
||||||
html += '<span style="color: #8b949e; margin: 0 4px;">→</span>';
|
html += '<span style="color: #8b949e; margin: 0 4px;">→</span>';
|
||||||
}}
|
}}
|
||||||
|
|
||||||
html += `<span class="category-badge ${{newClass}}">${{change.new_category}}</span>`;
|
html += `<span class="category-badge ${{newClass}}">${{change.new_category}}</span>`;
|
||||||
html += `<div class="timeline-time">${{timestamp}}</div>`;
|
html += `<div class="timeline-time">${{timestamp}}</div>`;
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
@@ -1486,10 +1601,10 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
|
|||||||
|
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
// Reputation column
|
// Reputation column
|
||||||
html += '<div class="timeline-column">';
|
html += '<div class="timeline-column">';
|
||||||
|
|
||||||
if (stats.list_on && Object.keys(stats.list_on).length > 0) {{
|
if (stats.list_on && Object.keys(stats.list_on).length > 0) {{
|
||||||
// Filter out is_hosting and is_proxy from the displayed list
|
// Filter out is_hosting and is_proxy from the displayed list
|
||||||
const filteredList = Object.entries(stats.list_on).filter(([source, data]) =>
|
const filteredList = Object.entries(stats.list_on).filter(([source, data]) =>
|
||||||
@@ -1524,7 +1639,7 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
|
|||||||
html += '<div class="timeline-header">Reputation</div>';
|
html += '<div class="timeline-header">Reputation</div>';
|
||||||
html += '<span class="reputation-clean" title="Not found on public blacklists">✓ Clean</span>';
|
html += '<span class="reputation-clean" title="Not found on public blacklists">✓ Clean</span>';
|
||||||
}}
|
}}
|
||||||
|
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
@@ -1728,23 +1843,23 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
|
|||||||
document.querySelectorAll('.tab-content').forEach(tab => {{
|
document.querySelectorAll('.tab-content').forEach(tab => {{
|
||||||
tab.classList.remove('active');
|
tab.classList.remove('active');
|
||||||
}});
|
}});
|
||||||
|
|
||||||
// Remove active class from all buttons
|
// Remove active class from all buttons
|
||||||
document.querySelectorAll('.tab-button').forEach(btn => {{
|
document.querySelectorAll('.tab-button').forEach(btn => {{
|
||||||
btn.classList.remove('active');
|
btn.classList.remove('active');
|
||||||
}});
|
}});
|
||||||
|
|
||||||
// Show selected tab
|
// Show selected tab
|
||||||
const selectedTab = document.getElementById(tabName);
|
const selectedTab = document.getElementById(tabName);
|
||||||
const selectedButton = document.querySelector(`.tab-button[href="#${{tabName}}"]`);
|
const selectedButton = document.querySelector(`.tab-button[href="#${{tabName}}"]`);
|
||||||
|
|
||||||
if (selectedTab) {{
|
if (selectedTab) {{
|
||||||
selectedTab.classList.add('active');
|
selectedTab.classList.add('active');
|
||||||
}}
|
}}
|
||||||
if (selectedButton) {{
|
if (selectedButton) {{
|
||||||
selectedButton.classList.add('active');
|
selectedButton.classList.add('active');
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// Load data for this tab
|
// Load data for this tab
|
||||||
if (tabName === 'ip-stats') {{
|
if (tabName === 'ip-stats') {{
|
||||||
loadIpStatistics(1);
|
loadIpStatistics(1);
|
||||||
@@ -1786,7 +1901,7 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
|
|||||||
if (e.target.classList.contains('sortable') && e.target.closest('#ip-stats-tbody')) {{
|
if (e.target.classList.contains('sortable') && e.target.closest('#ip-stats-tbody')) {{
|
||||||
return; // Don't sort when inside tbody
|
return; // Don't sort when inside tbody
|
||||||
}}
|
}}
|
||||||
|
|
||||||
const sortHeader = e.target.closest('th.sortable');
|
const sortHeader = e.target.closest('th.sortable');
|
||||||
if (!sortHeader) return;
|
if (!sortHeader) return;
|
||||||
|
|
||||||
@@ -1794,7 +1909,7 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
|
|||||||
if (!table || !table.classList.contains('ip-stats-table')) return;
|
if (!table || !table.classList.contains('ip-stats-table')) return;
|
||||||
|
|
||||||
const sortField = sortHeader.getAttribute('data-sort');
|
const sortField = sortHeader.getAttribute('data-sort');
|
||||||
|
|
||||||
// Toggle sort order if clicking the same field
|
// Toggle sort order if clicking the same field
|
||||||
if (currentSortBy === sortField) {{
|
if (currentSortBy === sortField) {{
|
||||||
currentSortOrder = currentSortOrder === 'desc' ? 'asc' : 'desc';
|
currentSortOrder = currentSortOrder === 'desc' ? 'asc' : 'desc';
|
||||||
@@ -1825,9 +1940,9 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
|
|||||||
console.error('IP stats tbody not found');
|
console.error('IP stats tbody not found');
|
||||||
return;
|
return;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
tbody.innerHTML = '<tr><td colspan="6" style="text-align: center;">Loading...</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="6" style="text-align: center;">Loading...</td></tr>';
|
||||||
|
|
||||||
try {{
|
try {{
|
||||||
console.log('Fetching attackers from page:', page, 'sort:', currentSortBy, currentSortOrder);
|
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, {{
|
const response = await fetch(DASHBOARD_PATH + '/api/attackers?page=' + page + '&page_size=' + PAGE_SIZE + '&sort_by=' + currentSortBy + '&sort_order=' + currentSortOrder, {{
|
||||||
@@ -1837,14 +1952,14 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
|
|||||||
'Pragma': 'no-cache'
|
'Pragma': 'no-cache'
|
||||||
}}
|
}}
|
||||||
}});
|
}});
|
||||||
|
|
||||||
console.log('Response status:', response.status);
|
console.log('Response status:', response.status);
|
||||||
|
|
||||||
if (!response.ok) throw new Error(`HTTP ${{response.status}}`);
|
if (!response.ok) throw new Error(`HTTP ${{response.status}}`);
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('Received data:', data);
|
console.log('Received data:', data);
|
||||||
|
|
||||||
if (!data.attackers || data.attackers.length === 0) {{
|
if (!data.attackers || data.attackers.length === 0) {{
|
||||||
tbody.innerHTML = '<tr><td colspan="6" style="text-align: center;">No attackers on this page.</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="6" style="text-align: center;">No attackers on this page.</td></tr>';
|
||||||
currentPage = page;
|
currentPage = page;
|
||||||
@@ -1852,7 +1967,7 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
|
|||||||
updatePaginationControls();
|
updatePaginationControls();
|
||||||
return;
|
return;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// Update pagination info
|
// Update pagination info
|
||||||
currentPage = data.pagination.page;
|
currentPage = data.pagination.page;
|
||||||
totalPages = data.pagination.total_pages;
|
totalPages = data.pagination.total_pages;
|
||||||
@@ -1860,7 +1975,7 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
|
|||||||
document.getElementById('total-pages').textContent = totalPages;
|
document.getElementById('total-pages').textContent = totalPages;
|
||||||
document.getElementById('total-attackers').textContent = data.pagination.total_attackers;
|
document.getElementById('total-attackers').textContent = data.pagination.total_attackers;
|
||||||
updatePaginationControls();
|
updatePaginationControls();
|
||||||
|
|
||||||
let html = '';
|
let html = '';
|
||||||
data.attackers.forEach((attacker, index) => {{
|
data.attackers.forEach((attacker, index) => {{
|
||||||
const rank = (currentPage - 1) * PAGE_SIZE + index + 1;
|
const rank = (currentPage - 1) * PAGE_SIZE + index + 1;
|
||||||
@@ -1880,10 +1995,10 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
|
|||||||
</td>
|
</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}});
|
}});
|
||||||
|
|
||||||
tbody.innerHTML = html;
|
tbody.innerHTML = html;
|
||||||
console.log('Populated', data.attackers.length, 'attacker records');
|
console.log('Populated', data.attackers.length, 'attacker records');
|
||||||
|
|
||||||
// Re-attach click listeners for expandable rows
|
// Re-attach click listeners for expandable rows
|
||||||
attachAttackerClickListeners();
|
attachAttackerClickListeners();
|
||||||
}} catch (err) {{
|
}} catch (err) {{
|
||||||
@@ -1895,7 +2010,7 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
|
|||||||
function updatePaginationControls() {{
|
function updatePaginationControls() {{
|
||||||
const prevBtn = document.getElementById('prev-page-btn');
|
const prevBtn = document.getElementById('prev-page-btn');
|
||||||
const nextBtn = document.getElementById('next-page-btn');
|
const nextBtn = document.getElementById('next-page-btn');
|
||||||
|
|
||||||
if (prevBtn) prevBtn.disabled = currentPage <= 1;
|
if (prevBtn) prevBtn.disabled = currentPage <= 1;
|
||||||
if (nextBtn) nextBtn.disabled = currentPage >= totalPages;
|
if (nextBtn) nextBtn.disabled = currentPage >= totalPages;
|
||||||
}}
|
}}
|
||||||
@@ -2167,7 +2282,7 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
|
|||||||
async function loadOverviewTable(tableId) {{
|
async function loadOverviewTable(tableId) {{
|
||||||
const config = tableConfig[tableId];
|
const config = tableConfig[tableId];
|
||||||
if (!config) return;
|
if (!config) return;
|
||||||
|
|
||||||
const state = overviewState[tableId];
|
const state = overviewState[tableId];
|
||||||
const tbody = document.getElementById(tableId + '-tbody');
|
const tbody = document.getElementById(tableId + '-tbody');
|
||||||
if (!tbody) return;
|
if (!tbody) return;
|
||||||
@@ -2201,7 +2316,7 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
|
|||||||
let html = '';
|
let html = '';
|
||||||
items.forEach((item, index) => {{
|
items.forEach((item, index) => {{
|
||||||
const rank = (state.currentPage - 1) * 5 + index + 1;
|
const rank = (state.currentPage - 1) * 5 + index + 1;
|
||||||
|
|
||||||
if (tableId === 'honeypot') {{
|
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-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;">
|
html += `<tr class="ip-stats-row" id="stats-row-honeypot-${{item.ip.replace(/\\./g, '-')}}" style="display: none;">
|
||||||
@@ -2347,12 +2462,12 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
|
|||||||
async function showIpDetail(ip) {{
|
async function showIpDetail(ip) {{
|
||||||
const modal = document.getElementById('ip-detail-modal');
|
const modal = document.getElementById('ip-detail-modal');
|
||||||
const bodyDiv = document.getElementById('ip-detail-body');
|
const bodyDiv = document.getElementById('ip-detail-body');
|
||||||
|
|
||||||
if (!modal || !bodyDiv) return;
|
if (!modal || !bodyDiv) return;
|
||||||
|
|
||||||
bodyDiv.innerHTML = '<div class="loading" style="text-align: center;">Loading IP details...</div>';
|
bodyDiv.innerHTML = '<div class="loading" style="text-align: center;">Loading IP details...</div>';
|
||||||
modal.classList.add('show');
|
modal.classList.add('show');
|
||||||
|
|
||||||
try {{
|
try {{
|
||||||
const response = await fetch(`${{DASHBOARD_PATH}}/api/ip-stats/${{ip}}`, {{
|
const response = await fetch(`${{DASHBOARD_PATH}}/api/ip-stats/${{ip}}`, {{
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
@@ -2361,9 +2476,9 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
|
|||||||
'Pragma': 'no-cache'
|
'Pragma': 'no-cache'
|
||||||
}}
|
}}
|
||||||
}});
|
}});
|
||||||
|
|
||||||
if (!response.ok) throw new Error(`HTTP ${{response.status}}`);
|
if (!response.ok) throw new Error(`HTTP ${{response.status}}`);
|
||||||
|
|
||||||
const stats = await response.json();
|
const stats = await response.json();
|
||||||
bodyDiv.innerHTML = '<h2>' + stats.ip + ' - Detailed Statistics</h2>' + formatIpStats(stats);
|
bodyDiv.innerHTML = '<h2>' + stats.ip + ' - Detailed Statistics</h2>' + formatIpStats(stats);
|
||||||
}} catch (err) {{
|
}} catch (err) {{
|
||||||
@@ -2790,7 +2905,7 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
|
|||||||
// Initialize map when Attacks tab is opened
|
// Initialize map when Attacks tab is opened
|
||||||
const originalSwitchTab = window.switchTab;
|
const originalSwitchTab = window.switchTab;
|
||||||
let attackTypesChartLoaded = false;
|
let attackTypesChartLoaded = false;
|
||||||
|
|
||||||
window.switchTab = function(tabName) {{
|
window.switchTab = function(tabName) {{
|
||||||
originalSwitchTab(tabName);
|
originalSwitchTab(tabName);
|
||||||
if (tabName === 'ip-stats') {{
|
if (tabName === 'ip-stats') {{
|
||||||
@@ -2823,7 +2938,7 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
|
|||||||
}});
|
}});
|
||||||
|
|
||||||
if (!response.ok) throw new Error('Failed to fetch attack types');
|
if (!response.ok) throw new Error('Failed to fetch attack types');
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const attacks = data.attacks || [];
|
const attacks = data.attacks || [];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user