upload
This commit is contained in:
		
							
								
								
									
										477
									
								
								logmon.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										477
									
								
								logmon.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,477 @@ | ||||
| #!/usr/bin/env python3 | ||||
| """ | ||||
| LogMon - Modularny demon do monitorowania logów i blokowania IP | ||||
| Autor: System Administrator | ||||
| Wersja: 1.0 | ||||
| """ | ||||
|  | ||||
| import sys | ||||
| import os | ||||
| import time | ||||
| import signal | ||||
| import logging | ||||
| import argparse | ||||
| import configparser | ||||
| from collections import defaultdict, deque | ||||
| from datetime import datetime, timedelta | ||||
| from pathlib import Path | ||||
|  | ||||
| # Importy z lokalnych modułów | ||||
| from modules import PostfixModule | ||||
| from backends import CSFBackend, NFTablesBackend, IPTablesBackend, UFWBackend | ||||
|  | ||||
|  | ||||
| class LogMonDaemon: | ||||
|     """Główny demon LogMon""" | ||||
|      | ||||
|     def __init__(self, config_file): | ||||
|         """ | ||||
|         Inicjalizacja demona | ||||
|          | ||||
|         Args: | ||||
|             config_file: Ścieżka do pliku konfiguracyjnego INI | ||||
|         """ | ||||
|         self.config = configparser.ConfigParser() | ||||
|         self.config.read(config_file) | ||||
|          | ||||
|         self.running = False | ||||
|         self.ip_tracker = defaultdict(lambda: deque()) | ||||
|         self.banned_ips = {} | ||||
|          | ||||
|         # Konfiguracja logowania | ||||
|         self.setup_logging() | ||||
|          | ||||
|         # Wybór backendu firewall | ||||
|         self.backend = self.load_backend() | ||||
|          | ||||
|         # Ładowanie modułów monitorowania | ||||
|         self.modules = self.load_modules() | ||||
|          | ||||
|         # Obsługa sygnałów systemowych | ||||
|         signal.signal(signal.SIGTERM, self.signal_handler) | ||||
|         signal.signal(signal.SIGINT, self.signal_handler) | ||||
|         signal.signal(signal.SIGHUP, self.signal_reload) | ||||
|          | ||||
|         self.logger.info("LogMon daemon initialized") | ||||
|          | ||||
|     def setup_logging(self): | ||||
|         """Konfiguracja systemu logowania""" | ||||
|         debug = self.config.getboolean('general', 'debug', fallback=False) | ||||
|         log_file = self.config.get('general', 'log_file',  | ||||
|                                    fallback='/var/log/logmon.log') | ||||
|          | ||||
|         level = logging.DEBUG if debug else logging.INFO | ||||
|          | ||||
|         # Usuń istniejące handlery | ||||
|         logging.basicConfig( | ||||
|             level=level, | ||||
|             format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', | ||||
|             handlers=[ | ||||
|                 logging.FileHandler(log_file), | ||||
|                 logging.StreamHandler(sys.stdout) | ||||
|             ] | ||||
|         ) | ||||
|          | ||||
|         self.logger = logging.getLogger('LogMon') | ||||
|         self.logger.info(f"Logging initialized (debug={debug})") | ||||
|          | ||||
|     def load_backend(self): | ||||
|         """Ładuje odpowiedni backend firewall""" | ||||
|         backend_name = self.config.get('general', 'backend', fallback='csf') | ||||
|          | ||||
|         self.logger.info(f"Loading backend: {backend_name}") | ||||
|          | ||||
|         backend_map = { | ||||
|             'csf': CSFBackend, | ||||
|             'nftables': NFTablesBackend, | ||||
|             'iptables': IPTablesBackend, | ||||
|             'ufw': UFWBackend | ||||
|         } | ||||
|          | ||||
|         if backend_name not in backend_map: | ||||
|             raise ValueError(f"Unknown backend: {backend_name}") | ||||
|              | ||||
|         backend_class = backend_map[backend_name] | ||||
|         backend = backend_class(self.config) | ||||
|          | ||||
|         # Sprawdź dostępność | ||||
|         if not backend.test_availability(): | ||||
|             self.logger.warning(f"Backend {backend_name} may not be available!") | ||||
|              | ||||
|         return backend | ||||
|          | ||||
|     def load_modules(self): | ||||
|         """Ładuje moduły monitorowania""" | ||||
|         modules = [] | ||||
|          | ||||
|         # Postfix module | ||||
|         if self.config.getboolean('module_postfix', 'enabled', fallback=False): | ||||
|             try: | ||||
|                 module = PostfixModule(self.config, self) | ||||
|                 modules.append(module) | ||||
|                 self.logger.info("Loaded Postfix module") | ||||
|             except Exception as e: | ||||
|                 self.logger.error(f"Error loading Postfix module: {e}") | ||||
|                  | ||||
|         # Tutaj można dodać więcej modułów w przyszłości | ||||
|         # if self.config.getboolean('module_ssh', 'enabled', fallback=False): | ||||
|         #     modules.append(SSHModule(self.config, self)) | ||||
|          | ||||
|         if not modules: | ||||
|             self.logger.warning("No modules loaded!") | ||||
|              | ||||
|         return modules | ||||
|          | ||||
|     def signal_handler(self, signum, frame): | ||||
|         """Obsługa sygnałów SIGTERM i SIGINT""" | ||||
|         self.logger.info(f"Received signal {signum}, shutting down gracefully...") | ||||
|         self.running = False | ||||
|          | ||||
|     def signal_reload(self, signum, frame): | ||||
|         """Obsługa sygnału SIGHUP - reload konfiguracji""" | ||||
|         self.logger.info("Received SIGHUP, reloading configuration...") | ||||
|         # TODO: Implementacja reload konfiguracji | ||||
|          | ||||
|     def track_failure(self, ip, score=1): | ||||
|         """ | ||||
|         Śledzi nieudane próby logowania dla danego IP | ||||
|          | ||||
|         Args: | ||||
|             ip: Adres IP atakującego | ||||
|             score: Punkty za dane niepowodzenie (domyślnie 1) | ||||
|         """ | ||||
|         now = datetime.now() | ||||
|          | ||||
|         # Dodaj wpis do trackera | ||||
|         self.ip_tracker[ip].append((now, score)) | ||||
|          | ||||
|         # Usuń stare wpisy spoza okna czasowego | ||||
|         time_window = self.config.getint('module_postfix', 'time_window',  | ||||
|                                          fallback=60) | ||||
|         cutoff = now - timedelta(seconds=time_window) | ||||
|          | ||||
|         while self.ip_tracker[ip] and self.ip_tracker[ip][0][0] < cutoff: | ||||
|             self.ip_tracker[ip].popleft() | ||||
|              | ||||
|         # Oblicz całkowity wynik | ||||
|         total_score = sum(score for _, score in self.ip_tracker[ip]) | ||||
|         failures_count = len(self.ip_tracker[ip]) | ||||
|          | ||||
|         max_failures = self.config.getint('module_postfix', 'max_failures',  | ||||
|                                          fallback=5) | ||||
|          | ||||
|         self.logger.debug( | ||||
|             f"IP {ip}: {failures_count} failures, score {total_score}/{max_failures}" | ||||
|         ) | ||||
|          | ||||
|         # Sprawdź czy przekroczono limit | ||||
|         if total_score >= max_failures and ip not in self.banned_ips: | ||||
|             self.ban_ip(ip) | ||||
|         elif total_score >= max_failures and ip in self.banned_ips: | ||||
|             self.logger.debug(f"IP {ip} already banned, ignoring") | ||||
|              | ||||
|     def ban_ip(self, ip): | ||||
|         """ | ||||
|         Banuje adres IP | ||||
|          | ||||
|         Args: | ||||
|             ip: Adres IP do zbanowania | ||||
|         """ | ||||
|         ban_duration = self.config.getint('module_postfix', 'ban_duration',  | ||||
|                                          fallback=86400) | ||||
|          | ||||
|         self.logger.warning( | ||||
|             f"Banning IP {ip} for {ban_duration} seconds " | ||||
|             f"({ban_duration/3600:.1f} hours)" | ||||
|         ) | ||||
|          | ||||
|         if self.backend.ban_ip(ip, ban_duration): | ||||
|             expiry_time = datetime.now() + timedelta(seconds=ban_duration) | ||||
|             self.banned_ips[ip] = expiry_time | ||||
|              | ||||
|             self.logger.info( | ||||
|                 f"Successfully banned {ip} until {expiry_time.strftime('%Y-%m-%d %H:%M:%S')}" | ||||
|             ) | ||||
|              | ||||
|             # Wyczyść tracker dla tego IP | ||||
|             if ip in self.ip_tracker: | ||||
|                 del self.ip_tracker[ip] | ||||
|         else: | ||||
|             self.logger.error(f"Failed to ban {ip}") | ||||
|              | ||||
|     def unban_ip(self, ip): | ||||
|         """ | ||||
|         Odbania adres IP | ||||
|          | ||||
|         Args: | ||||
|             ip: Adres IP do odbanowania | ||||
|         """ | ||||
|         self.logger.info(f"Unbanning expired IP {ip}") | ||||
|          | ||||
|         if self.backend.unban_ip(ip): | ||||
|             if ip in self.banned_ips: | ||||
|                 del self.banned_ips[ip] | ||||
|             self.logger.info(f"Successfully unbanned {ip}") | ||||
|         else: | ||||
|             self.logger.error(f"Failed to unban {ip}") | ||||
|              | ||||
|     def unban_expired(self): | ||||
|         """Usuwa bany, które wygasły""" | ||||
|         now = datetime.now() | ||||
|         expired = [ip for ip, expiry in self.banned_ips.items()  | ||||
|                    if now >= expiry] | ||||
|          | ||||
|         for ip in expired: | ||||
|             self.unban_ip(ip) | ||||
|              | ||||
|     def save_state(self): | ||||
|         """Zapisuje stan banów do pliku (opcjonalnie)""" | ||||
|         # TODO: Implementacja persystencji stanu | ||||
|         pass | ||||
|          | ||||
|     def load_state(self): | ||||
|         """Ładuje stan banów z pliku (opcjonalnie)""" | ||||
|         # TODO: Implementacja persystencji stanu | ||||
|         pass | ||||
|          | ||||
|     def daemonize(self): | ||||
|         """Przechodzi w tryb demona (fork)""" | ||||
|         pid_file = self.config.get('general', 'pid_file',  | ||||
|                                    fallback='/var/run/logmon.pid') | ||||
|          | ||||
|         # Sprawdź czy już działa | ||||
|         if os.path.exists(pid_file): | ||||
|             with open(pid_file, 'r') as f: | ||||
|                 old_pid = int(f.read().strip()) | ||||
|                  | ||||
|             # Sprawdź czy proces istnieje | ||||
|             try: | ||||
|                 os.kill(old_pid, 0) | ||||
|                 self.logger.error(f"Daemon already running with PID {old_pid}") | ||||
|                 sys.exit(1) | ||||
|             except OSError: | ||||
|                 # Proces nie istnieje, usuń stary PID file | ||||
|                 os.remove(pid_file) | ||||
|          | ||||
|         # Pierwszy fork | ||||
|         try: | ||||
|             pid = os.fork() | ||||
|             if pid > 0: | ||||
|                 # Parent process | ||||
|                 sys.exit(0) | ||||
|         except OSError as e: | ||||
|             self.logger.error(f"Fork #1 failed: {e}") | ||||
|             sys.exit(1) | ||||
|              | ||||
|         # Odłącz od terminala | ||||
|         os.chdir('/') | ||||
|         os.setsid() | ||||
|         os.umask(0) | ||||
|          | ||||
|         # Drugi fork | ||||
|         try: | ||||
|             pid = os.fork() | ||||
|             if pid > 0: | ||||
|                 # Parent process | ||||
|                 sys.exit(0) | ||||
|         except OSError as e: | ||||
|             self.logger.error(f"Fork #2 failed: {e}") | ||||
|             sys.exit(1) | ||||
|              | ||||
|         # Zapisz PID | ||||
|         pid = os.getpid() | ||||
|         with open(pid_file, 'w') as f: | ||||
|             f.write(str(pid)) | ||||
|              | ||||
|         self.logger.info(f"Daemon started with PID {pid}") | ||||
|          | ||||
|         # Przekieruj standardowe wyjścia | ||||
|         sys.stdout.flush() | ||||
|         sys.stderr.flush() | ||||
|          | ||||
|         devnull = open(os.devnull, 'r+') | ||||
|         os.dup2(devnull.fileno(), sys.stdin.fileno()) | ||||
|          | ||||
|         # Stdout i stderr zostawiamy dla logowania | ||||
|          | ||||
|     def cleanup(self): | ||||
|         """Sprzątanie przed zakończeniem""" | ||||
|         pid_file = self.config.get('general', 'pid_file',  | ||||
|                                    fallback='/var/run/logmon.pid') | ||||
|          | ||||
|         # Usuń PID file | ||||
|         if os.path.exists(pid_file): | ||||
|             try: | ||||
|                 os.remove(pid_file) | ||||
|             except Exception as e: | ||||
|                 self.logger.error(f"Error removing PID file: {e}") | ||||
|                  | ||||
|     def print_status(self): | ||||
|         """Wyświetla status demona""" | ||||
|         print("\n=== LogMon Status ===") | ||||
|         print(f"Backend: {self.backend.__class__.__name__}") | ||||
|         print(f"Modules: {len(self.modules)}") | ||||
|         print(f"Currently banned IPs: {len(self.banned_ips)}") | ||||
|         print(f"Tracked IPs: {len(self.ip_tracker)}") | ||||
|          | ||||
|         if self.banned_ips: | ||||
|             print("\nBanned IPs:") | ||||
|             for ip, expiry in sorted(self.banned_ips.items(),  | ||||
|                                     key=lambda x: x[1]): | ||||
|                 remaining = (expiry - datetime.now()).total_seconds() | ||||
|                 if remaining > 0: | ||||
|                     print(f"  {ip:15s} - expires in {remaining/60:.1f} minutes") | ||||
|                 else: | ||||
|                     print(f"  {ip:15s} - EXPIRED") | ||||
|                      | ||||
|         print("\n") | ||||
|          | ||||
|     def run(self, daemonize=True): | ||||
|         """ | ||||
|         Główna pętla demona | ||||
|          | ||||
|         Args: | ||||
|             daemonize: Czy przejść w tryb demona (fork) | ||||
|         """ | ||||
|         if daemonize: | ||||
|             self.daemonize() | ||||
|              | ||||
|         self.running = True | ||||
|         self.logger.info("LogMon daemon started") | ||||
|          | ||||
|         # Uruchom moduły | ||||
|         for module in self.modules: | ||||
|             try: | ||||
|                 module.start() | ||||
|             except Exception as e: | ||||
|                 self.logger.error(f"Error starting module: {e}") | ||||
|                  | ||||
|         # Główna pętla | ||||
|         try: | ||||
|             while self.running: | ||||
|                 try: | ||||
|                     # Sprawdź wygasłe bany co 10 sekund | ||||
|                     self.unban_expired() | ||||
|                      | ||||
|                     # Wyświetl status co minutę (tylko w debug mode) | ||||
|                     if self.config.getboolean('general', 'debug', fallback=False): | ||||
|                         if int(time.time()) % 60 == 0: | ||||
|                             self.logger.debug( | ||||
|                                 f"Status: {len(self.banned_ips)} banned, " | ||||
|                                 f"{len(self.ip_tracker)} tracked" | ||||
|                             ) | ||||
|                      | ||||
|                     # Krótkie oczekiwanie | ||||
|                     time.sleep(10) | ||||
|                      | ||||
|                 except KeyboardInterrupt: | ||||
|                     self.logger.info("Keyboard interrupt received") | ||||
|                     break | ||||
|                 except Exception as e: | ||||
|                     self.logger.error(f"Error in main loop: {e}", exc_info=True) | ||||
|                     time.sleep(1) | ||||
|                      | ||||
|         finally: | ||||
|             # Zatrzymaj moduły | ||||
|             self.logger.info("Stopping modules...") | ||||
|             for module in self.modules: | ||||
|                 try: | ||||
|                     module.stop() | ||||
|                 except Exception as e: | ||||
|                     self.logger.error(f"Error stopping module: {e}") | ||||
|                      | ||||
|             # Sprzątanie | ||||
|             self.cleanup() | ||||
|              | ||||
|             self.logger.info("LogMon daemon stopped") | ||||
|  | ||||
|  | ||||
| def main(): | ||||
|     """Główna funkcja programu""" | ||||
|     parser = argparse.ArgumentParser( | ||||
|         description='LogMon - Log Monitoring and IP Blocking Daemon', | ||||
|         formatter_class=argparse.RawDescriptionHelpFormatter, | ||||
|         epilog=""" | ||||
| Przykłady użycia: | ||||
|   %(prog)s -c /etc/logmon/config.ini          # Uruchom jako demon | ||||
|   %(prog)s -c /etc/logmon/config.ini -f       # Uruchom w foreground | ||||
|   %(prog)s -c /etc/logmon/config.ini -f -d    # Uruchom w foreground z debugiem | ||||
|    | ||||
| Sygnały: | ||||
|   SIGTERM, SIGINT - graceful shutdown | ||||
|   SIGHUP          - reload konfiguracji (TODO) | ||||
|         """ | ||||
|     ) | ||||
|      | ||||
|     parser.add_argument( | ||||
|         '-c', '--config', | ||||
|         default='/etc/logmon/config.ini', | ||||
|         help='Ścieżka do pliku konfiguracyjnego (domyślnie: /etc/logmon/config.ini)' | ||||
|     ) | ||||
|      | ||||
|     parser.add_argument( | ||||
|         '-f', '--foreground', | ||||
|         action='store_true', | ||||
|         help='Uruchom w foreground (nie demonizuj)' | ||||
|     ) | ||||
|      | ||||
|     parser.add_argument( | ||||
|         '-d', '--debug', | ||||
|         action='store_true', | ||||
|         help='Włącz tryb debug (nadpisuje ustawienie z config.ini)' | ||||
|     ) | ||||
|      | ||||
|     parser.add_argument( | ||||
|         '-t', '--test', | ||||
|         action='store_true', | ||||
|         help='Test konfiguracji i wyjście' | ||||
|     ) | ||||
|      | ||||
|     parser.add_argument( | ||||
|         '--version', | ||||
|         action='version', | ||||
|         version='LogMon 1.0' | ||||
|     ) | ||||
|      | ||||
|     args = parser.parse_args() | ||||
|      | ||||
|     # Sprawdź czy plik konfiguracyjny istnieje | ||||
|     if not os.path.exists(args.config): | ||||
|         print(f"Error: Configuration file not found: {args.config}", file=sys.stderr) | ||||
|         sys.exit(1) | ||||
|          | ||||
|     # Sprawdź uprawnienia root | ||||
|     if os.geteuid() != 0 and not args.test: | ||||
|         print("Error: This program must be run as root", file=sys.stderr) | ||||
|         sys.exit(1) | ||||
|          | ||||
|     try: | ||||
|         # Inicjalizacja demona | ||||
|         daemon = LogMonDaemon(args.config) | ||||
|          | ||||
|         # Nadpisz debug jeśli podano w argumentach | ||||
|         if args.debug: | ||||
|             daemon.config.set('general', 'debug', 'true') | ||||
|             daemon.setup_logging() | ||||
|              | ||||
|         # Tryb testowy | ||||
|         if args.test: | ||||
|             print("Configuration test successful") | ||||
|             daemon.print_status() | ||||
|             sys.exit(0) | ||||
|              | ||||
|         # Uruchom demona | ||||
|         daemon.run(daemonize=not args.foreground) | ||||
|          | ||||
|     except KeyboardInterrupt: | ||||
|         print("\nInterrupted by user") | ||||
|         sys.exit(0) | ||||
|     except Exception as e: | ||||
|         print(f"Fatal error: {e}", file=sys.stderr) | ||||
|         import traceback | ||||
|         traceback.print_exc() | ||||
|         sys.exit(1) | ||||
|  | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     main() | ||||
		Reference in New Issue
	
	Block a user
	 Mateusz Gruszczyński
					Mateusz Gruszczyński