163 lines
5.8 KiB
Python
163 lines
5.8 KiB
Python
"""
|
|
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=<user@domain.pl>, 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
|