Merge pull request #3 from ptarrant/feat/logging-system
Add rotating file logging system with app and access loggers
This commit is contained in:
@@ -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
113
src/logger.py
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user