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
import logging
import random
import time
from http.server import BaseHTTPRequestHandler
from typing import Optional, List
from datetime import datetime
from config import Config
from tracker import AccessTracker
@@ -23,6 +23,8 @@ class Handler(BaseHTTPRequestHandler):
config: Config = None
tracker: AccessTracker = None
counter: int = 0
app_logger: logging.Logger = None
access_logger: logging.Logger = None
def _get_client_ip(self) -> str:
"""Extract client IP address from request, checking proxy headers first"""
@@ -202,14 +204,14 @@ class Handler(BaseHTTPRequestHandler):
client_ip = self._get_client_ip()
user_agent = self._get_user_agent()
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))
if content_length > 0:
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.
self.tracker.record_access(client_ip, self.path, user_agent, post_data)
@@ -226,7 +228,7 @@ class Handler(BaseHTTPRequestHandler):
pass
except Exception as e:
# 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:
"""Serve special paths like robots.txt, API endpoints, etc."""
@@ -307,9 +309,9 @@ class Handler(BaseHTTPRequestHandler):
# Client disconnected, ignore silently
pass
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
return False
def do_GET(self):
@@ -327,17 +329,17 @@ class Handler(BaseHTTPRequestHandler):
except BrokenPipeError:
pass
except Exception as e:
print(f"Error generating dashboard: {e}")
self.app_logger.error(f"Error generating dashboard: {e}")
return
self.tracker.record_access(client_ip, self.path, 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():
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.end_headers()
return
@@ -361,9 +363,9 @@ class Handler(BaseHTTPRequestHandler):
# Client disconnected, ignore silently
pass
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):
"""Override to customize logging"""
"""Override to customize logging - uses access logger"""
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 tracker import AccessTracker
from handler import Handler
from logger import initialize_logging, get_app_logger, get_access_logger
def print_usage():
@@ -40,13 +41,20 @@ def main():
print_usage()
exit(0)
# Initialize logging
initialize_logging()
app_logger = get_app_logger()
access_logger = get_access_logger()
config = Config.from_env()
tracker = AccessTracker()
Handler.config = config
Handler.tracker = tracker
Handler.counter = config.canary_token_tries
Handler.app_logger = app_logger
Handler.access_logger = access_logger
if len(sys.argv) == 2:
try:
@@ -54,29 +62,29 @@ def main():
Handler.webpages = f.readlines()
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
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:
print(f'Starting deception server on port {config.port}...')
print(f'Dashboard available at: {config.dashboard_secret_path}')
app_logger.info(f'Starting deception server on port {config.port}...')
app_logger.info(f'Dashboard available at: {config.dashboard_secret_path}')
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:
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)
print('Server started. Use <Ctrl-C> to stop.')
app_logger.info('Server started. Use <Ctrl-C> to stop.')
server.serve_forever()
except KeyboardInterrupt:
print('\nStopping server...')
app_logger.info('Stopping server...')
server.socket.close()
print('Server stopped')
app_logger.info('Server stopped')
except Exception as e:
print(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'Error starting HTTP server on port {config.port}: {e}')
app_logger.error(f'Make sure you are root, if needed, and that port {config.port} is open.')
exit(1)

View File

@@ -6,9 +6,10 @@ This allows easy customization without touching Python code.
"""
import json
import os
from pathlib import Path
from logger import get_app_logger
class Wordlists:
"""Loads and provides access to wordlists from wordlists.json"""
@@ -19,15 +20,15 @@ class Wordlists:
def _load_config(self):
"""Load wordlists from JSON file"""
config_path = Path(__file__).parent.parent / 'wordlists.json'
try:
with open(config_path, 'r') as f:
return json.load(f)
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()
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()
def _get_defaults(self):