diff --git a/.gitignore b/.gitignore index 6249e18..ed1f3d9 100644 --- a/.gitignore +++ b/.gitignore @@ -80,4 +80,7 @@ personal-values.yaml #exports dir (keeping .gitkeep so we have the dir) /exports/* -/src/exports/* \ No newline at end of file +/src/exports/* + +# tmux config +.tmux.conf \ No newline at end of file diff --git a/.tmux.conf b/.tmux.conf new file mode 100644 index 0000000..701ea12 --- /dev/null +++ b/.tmux.conf @@ -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 diff --git a/README.md b/README.md index d3371d7..12a50dd 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,8 @@ services: - TZ="Europe/Rome" volumes: - ./config.yaml:/app/config.yaml:ro + # bind mount for firewall exporters + - ./exports:/app/exports - krawl-data:/app/data 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_PROBABILITY_ERROR_CODES` | Error response probability (0-100%) | `0` | | `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_HTTP_RISKY_METHODS_THRESHOLD` | Threshold for risky HTTP methods detection | `0.1` | | `KRAWL_VIOLATED_ROBOTS_THRESHOLD` | Threshold for robots.txt violations | `0.1` | @@ -223,7 +226,7 @@ For example ```bash # Set canary token -export CONFIG_LOCATION="config.yaml" +export CONFIG_LOCATION="config.yaml" export KRAWL_CANARY_TOKEN_URL="http://your-canary-token-url" # 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 ## 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 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`. -## Customizing the wordlist +## Customizing the wordlist 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 - 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. @@ -356,8 +359,8 @@ Contributions welcome! Please: ## ⚠️ Disclaimer -**This is a deception/honeypot system.** -Deploy in isolated environments and monitor carefully for security events. +**This is a deception/honeypot system.** +Deploy in isolated environments and monitor carefully for security events. Use responsibly and in compliance with applicable laws and regulations. ## Star History diff --git a/config.yaml b/config.yaml index c29ebe4..40246db 100644 --- a/config.yaml +++ b/config.yaml @@ -25,6 +25,9 @@ dashboard: # secret_path: super-secret-dashboard-path secret_path: test +exports: + path: "exports" + database: path: "data/krawl.db" retention_days: 30 @@ -43,4 +46,4 @@ analyzer: crawl: infinite_pages_for_malicious: true max_pages_limit: 250 - ban_duration_seconds: 600 \ No newline at end of file + ban_duration_seconds: 600 diff --git a/docker-compose.yaml b/docker-compose.yaml index 233692b..44b534d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,4 +1,5 @@ --- +# THIS IS FOR DEVELOPMENT PURPOSES services: krawl: build: @@ -16,17 +17,13 @@ services: - ./config.yaml:/app/config.yaml:ro - ./logs:/app/logs - ./exports:/app/exports - - data:/app/data + - ./data:/app/data restart: unless-stopped develop: watch: - path: ./Dockerfile action: rebuild - path: ./src/ - action: sync+restart - target: /app/src + action: rebuild - path: ./docker-compose.yaml action: rebuild - -volumes: - data: diff --git a/docs/firewall-exporters.md b/docs/firewall-exporters.md new file mode 100644 index 0000000..85d88cd --- /dev/null +++ b/docs/firewall-exporters.md @@ -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 +``` diff --git a/helm/Chart.yaml b/helm/Chart.yaml index 839e4f7..d0c00cd 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -3,7 +3,7 @@ name: krawl-chart description: A Helm chart for Krawl honeypot server type: application version: 1.0.0 -appVersion: 1.0.1 +appVersion: 1.0.2 keywords: - honeypot - security diff --git a/src/config.py b/src/config.py index 3e5983f..b98a750 100644 --- a/src/config.py +++ b/src/config.py @@ -37,9 +37,12 @@ class Config: infinite_pages_for_malicious: bool = True # Infinite pages for malicious crawlers ban_duration_seconds: int = 600 # Ban duration in seconds for IPs exceeding limits + # exporter settings + exports_path: str = "exports" # Database settings database_path: str = "data/krawl.db" database_retention_days: int = 30 + exports_path: str = "data/exports" # Analyzer settings http_risky_methods_threshold: float = None @@ -150,6 +153,7 @@ class Config: canary = data.get("canary", {}) dashboard = data.get("dashboard", {}) api = data.get("api", {}) + exports = data.get("exports", {}) database = data.get("database", {}) behavior = data.get("behavior", {}) analyzer = data.get("analyzer") or {} @@ -185,6 +189,7 @@ class Config: canary_token_tries=canary.get("token_tries", 10), dashboard_secret_path=dashboard_path, probability_error_codes=behavior.get("probability_error_codes", 0), + exports_path=exports.get("path"), database_path=database.get("path", "data/krawl.db"), database_retention_days=database.get("retention_days", 30), http_risky_methods_threshold=analyzer.get( diff --git a/src/database.py b/src/database.py index beca629..1834802 100644 --- a/src/database.py +++ b/src/database.py @@ -147,7 +147,9 @@ class DatabaseManager: migrations_run.append("region") 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") if "timezone" not in columns: diff --git a/src/firewall/fwtype.py b/src/firewall/fwtype.py new file mode 100644 index 0000000..0e0e421 --- /dev/null +++ b/src/firewall/fwtype.py @@ -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""" diff --git a/src/firewall/iptables.py b/src/firewall/iptables.py new file mode 100644 index 0000000..159171e --- /dev/null +++ b/src/firewall/iptables.py @@ -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) diff --git a/src/firewall/raw.py b/src/firewall/raw.py new file mode 100644 index 0000000..e0c82fe --- /dev/null +++ b/src/firewall/raw.py @@ -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) diff --git a/src/geo_utils.py b/src/geo_utils.py index 34b5265..9c4538e 100644 --- a/src/geo_utils.py +++ b/src/geo_utils.py @@ -41,7 +41,9 @@ def fetch_ip_geolocation(ip_address: str) -> Optional[Dict[str, Any]]: # Check if the API call was successful 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 # 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) most_recent = results[0] list_on = most_recent.get("list_on", {}) - + app_logger.debug(f"Fetched blocklist data for {ip_address}") return list_on except requests.RequestException as e: diff --git a/src/handler.py b/src/handler.py index 0a6abb2..e6da601 100644 --- a/src/handler.py +++ b/src/handler.py @@ -7,8 +7,17 @@ from datetime import datetime from http.server import BaseHTTPRequestHandler from typing import Optional, List 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 analyzer import Analyzer 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 xss_detector import detect_xss_pattern, generate_xss_response 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): @@ -58,10 +70,6 @@ class Handler(BaseHTTPRequestHandler): # Fallback to direct connection IP 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: """Get the category of an IP from the database""" 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] 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: """ Handle SQL injection honeypot endpoints. @@ -111,21 +114,20 @@ class Handler(BaseHTTPRequestHandler): try: # Get query parameters - query_string = self._parse_query_string() # Log SQL injection attempt 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 error_msg, content_type, status_code = generate_sql_error_response( - query_string or "" + request_query or "" ) if error_msg: # SQL injection detected - log and return error 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_header("Content-type", content_type) @@ -134,13 +136,13 @@ class Handler(BaseHTTPRequestHandler): else: # No injection detected - return fake data 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_header("Content-type", "application/json") self.end_headers() response_data = get_sql_response_with_data( - base_path, query_string or "" + base_path, request_query or "" ) self.wfile.write(response_data.encode()) @@ -239,11 +241,9 @@ class Handler(BaseHTTPRequestHandler): def do_POST(self): """Handle POST requests (mainly login attempts)""" client_ip = self._get_client_ip() - user_agent = self._get_user_agent() + user_agent = self.headers.get("User-Agent", "") post_data = "" - from urllib.parse import urlparse - base_path = urlparse(self.path).path if base_path in ["/api/search", "/api/sql", "/api/database"]: @@ -293,7 +293,6 @@ class Handler(BaseHTTPRequestHandler): for pair in post_data.split("&"): if "=" in pair: key, value = pair.split("=", 1) - from urllib.parse import unquote_plus parsed_data[unquote_plus(key)] = unquote_plus(value) @@ -486,18 +485,30 @@ class Handler(BaseHTTPRequestHandler): def do_GET(self): """Responds to webpage requests""" + client_ip = self._get_client_ip() + + # respond with HTTP error code if client is banned if self.tracker.is_banned_ip(client_ip): self.send_response(500) self.end_headers() 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 if self.config.dashboard_secret_path and self.path.startswith( f"{self.config.dashboard_secret_path}/static/" ): - import os file_path = self.path.replace( f"{self.config.dashboard_secret_path}/static/", "" @@ -543,8 +554,11 @@ class Handler(BaseHTTPRequestHandler): self.end_headers() try: stats = self.tracker.get_stats() - dashboard_path = self.config.dashboard_secret_path - self.wfile.write(generate_dashboard(stats, dashboard_path).encode()) + self.wfile.write( + generate_dashboard( + stats, self.config.dashboard_secret_path + ).encode() + ) except BrokenPipeError: pass except Exception as e: @@ -566,10 +580,7 @@ class Handler(BaseHTTPRequestHandler): self.send_header("Expires", "0") self.end_headers() try: - from database import get_database - import json - db = get_database() ip_stats_list = db.get_ip_stats(limit=500) self.wfile.write(json.dumps({"ips": ip_stats_list}).encode()) except BrokenPipeError: @@ -593,15 +604,7 @@ class Handler(BaseHTTPRequestHandler): self.send_header("Expires", "0") self.end_headers() 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_size = int(query_params.get("page_size", ["25"])[0]) sort_by = query_params.get("sort_by", ["total_requests"])[0] @@ -639,11 +642,6 @@ class Handler(BaseHTTPRequestHandler): self.send_header("Expires", "0") self.end_headers() 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) @@ -689,10 +687,7 @@ class Handler(BaseHTTPRequestHandler): self.send_header("Expires", "0") self.end_headers() try: - from database import get_database - import json - db = get_database() ip_stats = db.get_ip_stats_by_ip(ip_address) if ip_stats: self.wfile.write(json.dumps(ip_stats).encode()) @@ -719,11 +714,7 @@ class Handler(BaseHTTPRequestHandler): self.send_header("Expires", "0") self.end_headers() try: - from database import get_database - import json - from urllib.parse import urlparse, parse_qs - db = get_database() parsed_url = urlparse(self.path) query_params = parse_qs(parsed_url.query) page = int(query_params.get("page", ["1"])[0]) @@ -762,11 +753,7 @@ class Handler(BaseHTTPRequestHandler): self.send_header("Expires", "0") self.end_headers() try: - from database import get_database - import json - from urllib.parse import urlparse, parse_qs - db = get_database() parsed_url = urlparse(self.path) query_params = parse_qs(parsed_url.query) page = int(query_params.get("page", ["1"])[0]) @@ -805,11 +792,7 @@ class Handler(BaseHTTPRequestHandler): self.send_header("Expires", "0") self.end_headers() try: - from database import get_database - import json - from urllib.parse import urlparse, parse_qs - db = get_database() parsed_url = urlparse(self.path) query_params = parse_qs(parsed_url.query) page = int(query_params.get("page", ["1"])[0]) @@ -848,11 +831,7 @@ class Handler(BaseHTTPRequestHandler): self.send_header("Expires", "0") self.end_headers() try: - from database import get_database - import json - from urllib.parse import urlparse, parse_qs - db = get_database() parsed_url = urlparse(self.path) query_params = parse_qs(parsed_url.query) page = int(query_params.get("page", ["1"])[0]) @@ -891,11 +870,7 @@ class Handler(BaseHTTPRequestHandler): self.send_header("Expires", "0") self.end_headers() try: - from database import get_database - import json - from urllib.parse import urlparse, parse_qs - db = get_database() parsed_url = urlparse(self.path) query_params = parse_qs(parsed_url.query) page = int(query_params.get("page", ["1"])[0]) @@ -934,11 +909,7 @@ class Handler(BaseHTTPRequestHandler): self.send_header("Expires", "0") self.end_headers() try: - from database import get_database - import json - from urllib.parse import urlparse, parse_qs - db = get_database() parsed_url = urlparse(self.path) query_params = parse_qs(parsed_url.query) page = int(query_params.get("page", ["1"])[0]) @@ -963,13 +934,54 @@ class Handler(BaseHTTPRequestHandler): self.wfile.write(json.dumps({"error": str(e)}).encode()) 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 if ( self.config.dashboard_secret_path and self.path == f"{self.config.dashboard_secret_path}/api/download/malicious_ips.txt" ): - import os file_path = os.path.join( os.path.dirname(__file__), "exports", "malicious_ips.txt" diff --git a/src/tasks/fetch_ip_rep.py b/src/tasks/fetch_ip_rep.py index 8737fae..ddaea49 100644 --- a/src/tasks/fetch_ip_rep.py +++ b/src/tasks/fetch_ip_rep.py @@ -29,7 +29,7 @@ def main(): try: # Fetch geolocation data using ip-api.com geoloc_data = extract_geolocation_from_ip(ip) - + # Fetch blocklist data from lcrawl API blocklist_data = fetch_blocklist_data(ip) @@ -55,7 +55,7 @@ def main(): list_on = blocklist_data else: list_on = {} - + # Add flags to list_on list_on["is_proxy"] = is_proxy list_on["is_hosting"] = is_hosting @@ -69,7 +69,9 @@ def main(): sanitized_city = sanitize_for_storage(city, 100) if city else None sanitized_timezone = sanitize_for_storage(timezone, 50) 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) db_manager.update_ip_rep_infos( diff --git a/src/tasks/top_attacking_ips.py b/src/tasks/top_attacking_ips.py index c0cfbec..69d417b 100644 --- a/src/tasks/top_attacking_ips.py +++ b/src/tasks/top_attacking_ips.py @@ -4,9 +4,14 @@ import os from logger import get_app_logger from database import get_database from config import get_config -from models import IpStats +from models import IpStats, AccessLog 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() # ---------------------- @@ -20,7 +25,7 @@ TASK_CONFIG = { } 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 - config = get_config() server_ip = config.get_server_ip() public_ips = [ @@ -61,14 +65,24 @@ def main(): os.makedirs(EXPORTS_DIR, exist_ok=True) # Write IPs to file (one per line) - with open(OUTPUT_FILE, "w") as f: - for ip in public_ips: - f.write(f"{ip}\n") + for fwname in FWType._registry: - app_logger.info( - f"[Background Task] {task_name} exported {len(public_ips)} attacker IPs " - f"(filtered {len(attackers) - len(public_ips)} local/private IPs) to {OUTPUT_FILE}" - ) + # get banlist for specific ip + fw = FWType.create(fwname) + 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: app_logger.error(f"[Background Task] {task_name} failed: {e}") diff --git a/src/templates/dashboard_template.py b/src/templates/dashboard_template.py index be2537a..a31f929 100644 --- a/src/templates/dashboard_template.py +++ b/src/templates/dashboard_template.py @@ -9,6 +9,9 @@ import html from datetime import datetime from zoneinfo import ZoneInfo +# imports for the __init_subclass__ method, do not remove pls +from firewall import fwtype + def _escape(value) -> str: """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 suspicious_rows = ( - "\n".join([f"""