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
|
#!/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
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 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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user