Merge pull request #3 from ptarrant/feat/logging-system

Add rotating file logging system with app and access loggers
This commit is contained in:
Patrick Di Fazio
2025-12-27 00:22:44 +01:00
committed by GitHub
4 changed files with 157 additions and 33 deletions

View File

@@ -1,10 +1,10 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import logging
import random import random
import time import time
from http.server import BaseHTTPRequestHandler from http.server import BaseHTTPRequestHandler
from typing import Optional, List from typing import Optional, List
from datetime import datetime
from config import Config from config import Config
from tracker import AccessTracker from tracker import AccessTracker
@@ -23,6 +23,8 @@ class Handler(BaseHTTPRequestHandler):
config: Config = None config: Config = None
tracker: AccessTracker = None tracker: AccessTracker = None
counter: int = 0 counter: int = 0
app_logger: logging.Logger = None
access_logger: logging.Logger = None
def _get_client_ip(self) -> str: def _get_client_ip(self) -> str:
"""Extract client IP address from request, checking proxy headers first""" """Extract client IP address from request, checking proxy headers first"""
@@ -202,14 +204,14 @@ class Handler(BaseHTTPRequestHandler):
client_ip = self._get_client_ip() client_ip = self._get_client_ip()
user_agent = self._get_user_agent() user_agent = self._get_user_agent()
post_data = "" post_data = ""
print(f"[LOGIN ATTEMPT] {client_ip} - {self.path} - {user_agent[:50]}") self.access_logger.warning(f"[LOGIN ATTEMPT] {client_ip} - {self.path} - {user_agent[:50]}")
content_length = int(self.headers.get('Content-Length', 0)) content_length = int(self.headers.get('Content-Length', 0))
if content_length > 0: if content_length > 0:
post_data = self.rfile.read(content_length).decode('utf-8', errors="replace") post_data = self.rfile.read(content_length).decode('utf-8', errors="replace")
print(f"[POST DATA] {post_data[:200]}") self.access_logger.warning(f"[POST DATA] {post_data[:200]}")
# send the post data (body) to the record_access function so the post data can be used to detect suspicious things. # send the post data (body) to the record_access function so the post data can be used to detect suspicious things.
self.tracker.record_access(client_ip, self.path, user_agent, post_data) self.tracker.record_access(client_ip, self.path, user_agent, post_data)
@@ -226,7 +228,7 @@ class Handler(BaseHTTPRequestHandler):
pass pass
except Exception as e: except Exception as e:
# Log other exceptions but don't crash # Log other exceptions but don't crash
print(f"[ERROR] Failed to send response to {client_ip}: {str(e)}") self.app_logger.error(f"Failed to send response to {client_ip}: {str(e)}")
def serve_special_path(self, path: str) -> bool: def serve_special_path(self, path: str) -> bool:
"""Serve special paths like robots.txt, API endpoints, etc.""" """Serve special paths like robots.txt, API endpoints, etc."""
@@ -307,9 +309,9 @@ class Handler(BaseHTTPRequestHandler):
# Client disconnected, ignore silently # Client disconnected, ignore silently
pass pass
except Exception as e: except Exception as e:
print(f"[ERROR] Failed to serve special path {path}: {str(e)}") self.app_logger.error(f"Failed to serve special path {path}: {str(e)}")
pass pass
return False return False
def do_GET(self): def do_GET(self):
@@ -327,17 +329,17 @@ class Handler(BaseHTTPRequestHandler):
except BrokenPipeError: except BrokenPipeError:
pass pass
except Exception as e: except Exception as e:
print(f"Error generating dashboard: {e}") self.app_logger.error(f"Error generating dashboard: {e}")
return return
self.tracker.record_access(client_ip, self.path, user_agent) self.tracker.record_access(client_ip, self.path, user_agent)
if self.tracker.is_suspicious_user_agent(user_agent): if self.tracker.is_suspicious_user_agent(user_agent):
print(f"[SUSPICIOUS] {client_ip} - {user_agent[:50]} - {self.path}") self.access_logger.warning(f"[SUSPICIOUS] {client_ip} - {user_agent[:50]} - {self.path}")
if self._should_return_error(): if self._should_return_error():
error_code = self._get_random_error_code() error_code = self._get_random_error_code()
print(f"[ERROR] Returning {error_code} to {client_ip} - {self.path}") self.access_logger.info(f"Returning error {error_code} to {client_ip} - {self.path}")
self.send_response(error_code) self.send_response(error_code)
self.end_headers() self.end_headers()
return return
@@ -361,9 +363,9 @@ class Handler(BaseHTTPRequestHandler):
# Client disconnected, ignore silently # Client disconnected, ignore silently
pass pass
except Exception as e: except Exception as e:
print(f"Error generating page: {e}") self.app_logger.error(f"Error generating page: {e}")
def log_message(self, format, *args): def log_message(self, format, *args):
"""Override to customize logging""" """Override to customize logging - uses access logger"""
client_ip = self._get_client_ip() client_ip = self._get_client_ip()
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {client_ip} - {format % args}") self.access_logger.info(f"{client_ip} - {format % args}")

113
src/logger.py Normal file
View File

@@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""
Logging singleton module for the Krawl honeypot.
Provides two loggers: app (application) and access (HTTP access logs).
"""
import logging
import os
from logging.handlers import RotatingFileHandler
class LoggerManager:
"""Singleton logger manager for the application."""
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def initialize(self, log_dir: str = "logs") -> None:
"""
Initialize the logging system with rotating file handlers.
Args:
log_dir: Directory for log files (created if not exists)
"""
if self._initialized:
return
# Create log directory if it doesn't exist
os.makedirs(log_dir, exist_ok=True)
# Common format for all loggers
log_format = logging.Formatter(
"[%(asctime)s] %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
# Rotation settings: 1MB max, 5 backups
max_bytes = 1048576 # 1MB
backup_count = 5
# Setup application logger
self._app_logger = logging.getLogger("krawl.app")
self._app_logger.setLevel(logging.INFO)
self._app_logger.handlers.clear()
app_file_handler = RotatingFileHandler(
os.path.join(log_dir, "krawl.log"),
maxBytes=max_bytes,
backupCount=backup_count
)
app_file_handler.setFormatter(log_format)
self._app_logger.addHandler(app_file_handler)
app_stream_handler = logging.StreamHandler()
app_stream_handler.setFormatter(log_format)
self._app_logger.addHandler(app_stream_handler)
# Setup access logger
self._access_logger = logging.getLogger("krawl.access")
self._access_logger.setLevel(logging.INFO)
self._access_logger.handlers.clear()
access_file_handler = RotatingFileHandler(
os.path.join(log_dir, "access.log"),
maxBytes=max_bytes,
backupCount=backup_count
)
access_file_handler.setFormatter(log_format)
self._access_logger.addHandler(access_file_handler)
access_stream_handler = logging.StreamHandler()
access_stream_handler.setFormatter(log_format)
self._access_logger.addHandler(access_stream_handler)
self._initialized = True
@property
def app(self) -> logging.Logger:
"""Get the application logger."""
if not self._initialized:
self.initialize()
return self._app_logger
@property
def access(self) -> logging.Logger:
"""Get the access logger."""
if not self._initialized:
self.initialize()
return self._access_logger
# Module-level singleton instance
_logger_manager = LoggerManager()
def get_app_logger() -> logging.Logger:
"""Get the application logger instance."""
return _logger_manager.app
def get_access_logger() -> logging.Logger:
"""Get the access logger instance."""
return _logger_manager.access
def initialize_logging(log_dir: str = "logs") -> None:
"""Initialize the logging system."""
_logger_manager.initialize(log_dir)

View File

@@ -11,6 +11,7 @@ from http.server import HTTPServer
from config import Config from config import Config
from tracker import AccessTracker from tracker import AccessTracker
from handler import Handler from handler import Handler
from logger import initialize_logging, get_app_logger, get_access_logger
def print_usage(): def print_usage():
@@ -40,13 +41,20 @@ def main():
print_usage() print_usage()
exit(0) exit(0)
# Initialize logging
initialize_logging()
app_logger = get_app_logger()
access_logger = get_access_logger()
config = Config.from_env() config = Config.from_env()
tracker = AccessTracker() tracker = AccessTracker()
Handler.config = config Handler.config = config
Handler.tracker = tracker Handler.tracker = tracker
Handler.counter = config.canary_token_tries Handler.counter = config.canary_token_tries
Handler.app_logger = app_logger
Handler.access_logger = access_logger
if len(sys.argv) == 2: if len(sys.argv) == 2:
try: try:
@@ -54,29 +62,29 @@ def main():
Handler.webpages = f.readlines() Handler.webpages = f.readlines()
if not Handler.webpages: if not Handler.webpages:
print('The file provided was empty. Using randomly generated links.') app_logger.warning('The file provided was empty. Using randomly generated links.')
Handler.webpages = None Handler.webpages = None
except IOError: except IOError:
print('Can\'t read input file. Using randomly generated links.') app_logger.warning("Can't read input file. Using randomly generated links.")
try: try:
print(f'Starting deception server on port {config.port}...') app_logger.info(f'Starting deception server on port {config.port}...')
print(f'Dashboard available at: {config.dashboard_secret_path}') app_logger.info(f'Dashboard available at: {config.dashboard_secret_path}')
if config.canary_token_url: if config.canary_token_url:
print(f'Canary token will appear after {config.canary_token_tries} tries') app_logger.info(f'Canary token will appear after {config.canary_token_tries} tries')
else: else:
print('No canary token configured (set CANARY_TOKEN_URL to enable)') app_logger.info('No canary token configured (set CANARY_TOKEN_URL to enable)')
server = HTTPServer(('0.0.0.0', config.port), Handler) server = HTTPServer(('0.0.0.0', config.port), Handler)
print('Server started. Use <Ctrl-C> to stop.') app_logger.info('Server started. Use <Ctrl-C> to stop.')
server.serve_forever() server.serve_forever()
except KeyboardInterrupt: except KeyboardInterrupt:
print('\nStopping server...') app_logger.info('Stopping server...')
server.socket.close() server.socket.close()
print('Server stopped') app_logger.info('Server stopped')
except Exception as e: except Exception as e:
print(f'Error starting HTTP server on port {config.port}: {e}') app_logger.error(f'Error starting HTTP server on port {config.port}: {e}')
print(f'Make sure you are root, if needed, and that port {config.port} is open.') app_logger.error(f'Make sure you are root, if needed, and that port {config.port} is open.')
exit(1) exit(1)

View File

@@ -6,9 +6,10 @@ This allows easy customization without touching Python code.
""" """
import json import json
import os
from pathlib import Path from pathlib import Path
from logger import get_app_logger
class Wordlists: class Wordlists:
"""Loads and provides access to wordlists from wordlists.json""" """Loads and provides access to wordlists from wordlists.json"""
@@ -19,15 +20,15 @@ class Wordlists:
def _load_config(self): def _load_config(self):
"""Load wordlists from JSON file""" """Load wordlists from JSON file"""
config_path = Path(__file__).parent.parent / 'wordlists.json' config_path = Path(__file__).parent.parent / 'wordlists.json'
try: try:
with open(config_path, 'r') as f: with open(config_path, 'r') as f:
return json.load(f) return json.load(f)
except FileNotFoundError: except FileNotFoundError:
print(f"⚠️ Warning: {config_path} not found, using default values") get_app_logger().warning(f"Wordlists file {config_path} not found, using default values")
return self._get_defaults() return self._get_defaults()
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
print(f"⚠️ Warning: Invalid JSON in {config_path}: {e}") get_app_logger().warning(f"Invalid JSON in {config_path}: {e}")
return self._get_defaults() return self._get_defaults()
def _get_defaults(self): def _get_defaults(self):