diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backends/init.py b/backends/__init__.py similarity index 100% rename from backends/init.py rename to backends/__init__.py diff --git a/backends/__pycache__/__init__.cpython-312.pyc b/backends/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..89adbce Binary files /dev/null and b/backends/__pycache__/__init__.cpython-312.pyc differ diff --git a/backends/__pycache__/base.cpython-312.pyc b/backends/__pycache__/base.cpython-312.pyc new file mode 100644 index 0000000..fad4ac0 Binary files /dev/null and b/backends/__pycache__/base.cpython-312.pyc differ diff --git a/backends/__pycache__/csf.cpython-312.pyc b/backends/__pycache__/csf.cpython-312.pyc new file mode 100644 index 0000000..4f9582f Binary files /dev/null and b/backends/__pycache__/csf.cpython-312.pyc differ diff --git a/backends/__pycache__/iptables.cpython-312.pyc b/backends/__pycache__/iptables.cpython-312.pyc new file mode 100644 index 0000000..7eef698 Binary files /dev/null and b/backends/__pycache__/iptables.cpython-312.pyc differ diff --git a/backends/__pycache__/nftables.cpython-312.pyc b/backends/__pycache__/nftables.cpython-312.pyc new file mode 100644 index 0000000..dc29622 Binary files /dev/null and b/backends/__pycache__/nftables.cpython-312.pyc differ diff --git a/backends/__pycache__/ufw.cpython-312.pyc b/backends/__pycache__/ufw.cpython-312.pyc new file mode 100644 index 0000000..16874ee Binary files /dev/null and b/backends/__pycache__/ufw.cpython-312.pyc differ diff --git a/config.ini b/config.ini index aef3d57..8b7b0d3 100644 --- a/config.ini +++ b/config.ini @@ -1,42 +1,191 @@ +# ============================================ +# LogMon Configuration File +# ============================================ + [general] +# Tryb debug - wyświetla szczegółowe informacje debug = false + +# Ścieżka do pliku z logami LogMon log_file = /var/log/logmon.log + +# Plik PID demona pid_file = /var/run/logmon.pid + +# Backend firewall: csf, nftables, iptables, ufw backend = csf + +# ============================================ +# Konfiguracja backendów firewall +# ============================================ + [backend_csf] +# Ścieżka do wykonywania CSF csf_path = /usr/sbin/csf -# Dodatkowe opcje CSF [backend_nftables] -table_name = filter +# Nazwa tabeli i chain dla nftables +table_name = inet chain_name = logmon_block [backend_iptables] +# Nazwa chain dla iptables chain_name = LOGMON_BLOCK [backend_ufw] # UFW nie wymaga dodatkowych parametrów + +# ============================================ +# Moduł Postfix - SMTP Server +# ============================================ + [module_postfix] +# Włącz/wyłącz moduł enabled = true + +# Ścieżka do logu Postfix log_file = /var/log/mail.log -# Alternatywnie dla systemd: + +# Alternatywnie dla systemd journald: # use_journald = true # journald_unit = postfix.service -# Parametry detekcji +# Maksymalna liczba niepowodzeń przed banem max_failures = 5 + +# Okno czasowe w sekundach (domyślnie 60s = 1 minuta) time_window = 60 + +# Czas bana w sekundach (domyślnie 86400s = 24 godziny) ban_duration = 86400 -# Wzorce do wykrywania -patterns = auth_failed,sasl_failed +# Lista wzorców do wykrywania (oddzielone przecinkami) +patterns = postfix_auth_failed,postfix_sasl_failed -[pattern_auth_failed] + +# ============================================ +# Moduł Dovecot - IMAP/POP3 Server +# ============================================ + +[module_dovecot] +# Włącz/wyłącz moduł +enabled = true + +# Ścieżka do logu Dovecot +log_file = /var/log/dovecot-info.log + +# Maksymalna liczba niepowodzeń przed banem +max_failures = 5 + +# Okno czasowe w sekundach (domyślnie 120s = 2 minuty) +time_window = 120 + +# Czas bana w sekundach (domyślnie 86400s = 24 godziny) +ban_duration = 86400 + +# Ignoruj błędy SSL/TLS (często są to skanery, nie ataki brute-force) +ignore_ssl_errors = true + +# Ignoruj połączenia z localhost (127.0.0.1) +ignore_localhost = true + +# Lista wzorców do wykrywania +patterns = dovecot_auth_failed,dovecot_auth_failed_multi + + +# ============================================ +# Wzorce dla Postfix +# ============================================ + +[pattern_postfix_auth_failed] +# Wykrywa: "authentication failed" regex = authentication failed score = 1 -[pattern_sasl_failed] +[pattern_postfix_sasl_failed] +# Wykrywa: "SASL LOGIN authentication failed" i podobne regex = SASL [A-Z\-\d]+ authentication failed score = 2 + + +# ============================================ +# Wzorce dla Dovecot +# ============================================ + +[pattern_dovecot_auth_failed] +# Wykrywa: "auth failed, 1 attempts" +regex = auth failed, 1 attempts +score = 2 + +[pattern_dovecot_auth_failed_multi] +# Wykrywa: "auth failed, 2 attempts" lub więcej (2-9+) +regex = auth failed, [2-9]+ attempts +score = 5 + + +# ============================================ +# Dodatkowe moduły (przygotowane do rozbudowy) +# ============================================ + +# [module_ssh] +# enabled = false +# log_file = /var/log/auth.log +# max_failures = 5 +# time_window = 300 +# ban_duration = 3600 +# patterns = ssh_failed_password,ssh_invalid_user + +# [pattern_ssh_failed_password] +# regex = Failed password for .+ from +# score = 2 + +# [pattern_ssh_invalid_user] +# regex = Invalid user .+ from +# score = 3 + + +# [module_nginx] +# enabled = false +# log_file = /var/log/nginx/error.log +# max_failures = 10 +# time_window = 60 +# ban_duration = 3600 +# patterns = nginx_404_flood,nginx_403_scan + +# [pattern_nginx_404_flood] +# regex = \[error\].*GET .* HTTP/ +# score = 1 + +# [pattern_nginx_403_scan] +# regex = 403.*GET +# score = 2 + + +# ============================================ +# Whitelist IP (przygotowane do implementacji) +# ============================================ + +# [whitelist] +# # Lista IP które nigdy nie będą banowane (oddzielone przecinkami) +# ips = 127.0.0.1,192.168.1.0/24,10.0.0.0/8 +# +# # Lub z pliku: +# # file = /etc/logmon/whitelist.txt + + +# ============================================ +# Zaawansowane opcje +# ============================================ + +# [advanced] +# # Maksymalna liczba jednocześnie śledzonych IP +# max_tracked_ips = 10000 +# +# # Jak często sprawdzać wygasłe bany (w sekundach) +# check_expired_interval = 10 +# +# # Persystencja - zapisz stan banów do pliku +# persist_state = true +# persist_file = /var/lib/logmon/state.json diff --git a/logmon.py b/logmon.py index 4775add..46302c4 100644 --- a/logmon.py +++ b/logmon.py @@ -16,10 +16,12 @@ 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 +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +# Importy z lokalnych modułów +from modules import PostfixModule, DovecotModule +from backends import CSFBackend, NFTablesBackend, IPTablesBackend, UFWBackend class LogMonDaemon: """Główny demon LogMon""" @@ -112,7 +114,15 @@ class LogMonDaemon: self.logger.info("Loaded Postfix module") except Exception as e: self.logger.error(f"Error loading Postfix module: {e}") - + # Dovecot module + if self.config.getboolean('module_dovecot', 'enabled', fallback=False): + try: + module = DovecotModule(self.config, self) + modules.append(module) + self.logger.info("Loaded Dovecot module") + except Exception as e: + self.logger.error(f"Error loading Dovecot 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)) diff --git a/modules/init.py b/modules/__init__.py similarity index 58% rename from modules/init.py rename to modules/__init__.py index 14b94ec..8199461 100644 --- a/modules/init.py +++ b/modules/__init__.py @@ -4,5 +4,7 @@ LogMon Modules - Moduły monitorowania różnych aplikacji from .base import LogModule from .postfix import PostfixModule +from .dovecot import DovecotModule + +__all__ = ['LogModule', 'PostfixModule', 'DovecotModule'] -__all__ = ['LogModule', 'PostfixModule'] diff --git a/modules/__pycache__/__init__.cpython-312.pyc b/modules/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..faa4152 Binary files /dev/null and b/modules/__pycache__/__init__.cpython-312.pyc differ diff --git a/modules/__pycache__/base.cpython-312.pyc b/modules/__pycache__/base.cpython-312.pyc new file mode 100644 index 0000000..1e88f8c Binary files /dev/null and b/modules/__pycache__/base.cpython-312.pyc differ diff --git a/modules/__pycache__/postfix.cpython-312.pyc b/modules/__pycache__/postfix.cpython-312.pyc new file mode 100644 index 0000000..6dd1fc3 Binary files /dev/null and b/modules/__pycache__/postfix.cpython-312.pyc differ diff --git a/modules/dovecot.py b/modules/dovecot.py new file mode 100644 index 0000000..f15cbfa --- /dev/null +++ b/modules/dovecot.py @@ -0,0 +1,162 @@ +""" +Moduł monitorujący Dovecot IMAP/POP3 server +""" + +import re +import time +from .base import LogModule + + +class DovecotModule(LogModule): + """Moduł monitorujący Dovecot""" + + def __init__(self, config, daemon): + super().__init__(config, daemon) + + # Kompiluj wzorce regex dla wydajności + self.patterns = self._load_patterns() + + # Regex do wyciągania IP z logów Dovecot + # Obsługuje format: rip=IP, lip=IP + self.ip_pattern = re.compile(r'rip=(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})') + + # Ścieżka do pliku logu + self.log_file = config.get('module_dovecot', 'log_file', + fallback='/var/log/dovecot-info.log') + + # Czy ignorować błędy SSL/TLS (często są to skanery, nie ataki brute-force) + self.ignore_ssl_errors = config.getboolean('module_dovecot', 'ignore_ssl_errors', + fallback=True) + + # Czy ignorować połączenia z localhost + self.ignore_localhost = config.getboolean('module_dovecot', 'ignore_localhost', + fallback=True) + + self.logger.info(f"Loaded {len(self.patterns)} patterns for Dovecot") + if self.ignore_ssl_errors: + self.logger.info("SSL/TLS errors will be ignored") + + def _load_patterns(self): + """Ładuje wzorce z konfiguracji""" + patterns = [] + pattern_names = self.config.get('module_dovecot', 'patterns', + fallback='').split(',') + + for name in pattern_names: + name = name.strip() + if not name: + continue + + section = f'pattern_{name}' + if section not in self.config: + self.logger.warning(f"Pattern section '{section}' not found in config") + continue + + try: + regex = self.config.get(section, 'regex') + score = self.config.getint(section, 'score', fallback=1) + + patterns.append({ + 'name': name, + 'regex': re.compile(regex, re.IGNORECASE), + 'score': score + }) + + self.logger.debug(f"Loaded pattern '{name}': {regex} (score: {score})") + + except Exception as e: + self.logger.error(f"Error loading pattern '{name}': {e}") + + return patterns + + def _run(self): + """Główna pętla - tail -f na pliku logu""" + self.logger.info(f"Tailing log file: {self.log_file}") + + try: + with open(self.log_file, 'r') as f: + # Przejdź na koniec pliku + f.seek(0, 2) + + while self.running: + line = f.readline() + + if line: + self.process_line(line.strip()) + else: + # Brak nowych linii, czekaj chwilę + time.sleep(0.1) + + except FileNotFoundError: + self.logger.error(f"Log file not found: {self.log_file}") + except PermissionError: + self.logger.error(f"Permission denied reading: {self.log_file}") + except Exception as e: + self.logger.error(f"Error tailing log: {e}") + + def _is_ssl_error(self, line): + """Sprawdza czy linia zawiera błąd SSL/TLS""" + ssl_keywords = [ + 'SSL_accept() failed', + 'TLS handshaking', + 'unsupported protocol', + 'version too low', + 'no shared cipher', + 'wrong version number', + 'internal error', + 'Connection reset by peer', + 'bad key share', + 'unknown protocol', + 'http request' + ] + + line_lower = line.lower() + return any(keyword.lower() in line_lower for keyword in ssl_keywords) + + def process_line(self, line): + """ + Przetwarza linię z logu Dovecot + + Przykłady linii: + - imap-login: Info: Disconnected: Connection closed (auth failed, 1 attempts in 2 secs): user=, method=PLAIN, rip=1.2.3.4, lip=5.6.7.8, TLS + - imap-login: Info: Disconnected: Connection closed: SSL_accept() failed (no auth attempts): user=<>, rip=1.2.3.4 + """ + + # Ignoruj błędy SSL/TLS jeśli włączone + if self.ignore_ssl_errors and self._is_ssl_error(line): + return + + # Wyciągnij IP + ip_match = self.ip_pattern.search(line) + if not ip_match: + return + + ip = ip_match.group(1) + + # Pomiń localhost jeśli włączone + if self.ignore_localhost and (ip.startswith('127.') or ip == '::1'): + return + + # Pomiń lokalne IP + if ip.startswith('192.168.') or ip.startswith('10.') or ip.startswith('172.'): + # Sprawdź czy to nie jest 172.16-31.x.x (prywatne) + if ip.startswith('172.'): + second_octet = int(ip.split('.')[1]) + if 16 <= second_octet <= 31: + return + else: + return + + # Sprawdź wzorce + for pattern in self.patterns: + if pattern['regex'].search(line): + self.logger.debug( + f"Pattern '{pattern['name']}' matched for IP {ip}" + ) + self.logger.debug(f"Line: {line}") + + # Zgłoś niepowodzenie do demona + self.daemon.track_failure(ip, pattern['score']) + + # Tylko pierwszy pasujący wzorzec + break diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/init.py b/utils/init.py deleted file mode 100644 index a6c55a4..0000000 --- a/utils/init.py +++ /dev/null @@ -1,5 +0,0 @@ -""" -LogMon Utils - Narzędzia pomocnicze -""" - -__all__ = []