From 119027214371c381cfaee5964ff966dfa2b9df71 Mon Sep 17 00:00:00 2001 From: "Antonio J. Delgado" Date: Fri, 31 Jan 2025 14:41:34 +0200 Subject: [PATCH] Add initial script --- requirements.txt | 3 +- smtpd_watcher/smtpd_watcher.py | 128 +++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 66bf966..2dbd853 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ click -click_config_file \ No newline at end of file +click_config_file +mariadb diff --git a/smtpd_watcher/smtpd_watcher.py b/smtpd_watcher/smtpd_watcher.py index 2294a03..e35509d 100644 --- a/smtpd_watcher/smtpd_watcher.py +++ b/smtpd_watcher/smtpd_watcher.py @@ -9,11 +9,16 @@ import sys import os import logging from logging.handlers import SysLogHandler +import subprocess +import json +import re import click import click_config_file +import mariadb class SmtpdWatcher: + """SMTPd watcher for failed connections""" def __init__(self, **kwargs): self.config = kwargs @@ -30,6 +35,125 @@ class SmtpdWatcher: 'smtpd_watcher.log' ) self._init_log() + self.banned_ips = self._read_banned_ips() + self.mail_users = self._get_mail_user() + if self.config['mail_log_file'] == '-': + logfile = sys.stdin + else: + logfile = open(self.config['mail_log_file'], 'r', encoding='utf-8') + for line in logfile: + if not self._process_log_file(line): + sys.exit(1) + + def _read_banned_ips(self): + ips = {} + result = subprocess.run( + ['/usr/bin/fail2ban-client', 'get', 'postfix', 'banned'], + check=True, + capture_output=True, + ) + ips['postfix'] = json.loads(result.stdout) + result = subprocess.run( + ['/usr/bin/fail2ban-client', 'get', 'postfix-sasl', 'banned'], + check=True, + capture_output=True, + ) + ips['postfix-sasl'] = json.loads(result.stdout) + result = subprocess.run( + ['ufw', 'status', 'numbered'], + check=True, + capture_output=True, + ) + ips['ufw'] = [] + for line in result.stdout: + if 'DENY IN' in line: + split_line = line.split(' ') + ips['ufw'].append(split_line[4]) + return ips + + def _process_log_file(self, line): + ban = False + if 'authentication failure' in line: + ip_match = re.search(r'\[([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})\]', line) + if ip_match: + ip = ip_match.group(1) + else: + return False + target_user_match = re.search(r'sasl_username=([^ ]*)', line) + if target_user_match: + target_user = target_user_match.group(1) + if not self._check_mail_user(target_user): + ban = True + else: + ban = True + if ban: + if ip not in self.banned_ips['postfix']: + self._fail2ban_ban_ip('postfix', ip) + if ip not in self.banned_ips['postfix-sasl']: + self._fail2ban_ban_ip('postfix-sasl', ip) + if ip not in self.banned_ips['ufw']: + self._ufw_deny_ip(ip) + return True + + def _ufw_deny_ip(self, ip): + result = subprocess.run( + ['/usr/sbin/ufw', 'deny', 'from', ip], + check=True, + capture_output=True, + ) + self._log.debug( + "Denying traffic from IP '%s' in UFW result: %s", + ip, + result.stdout + ) + if result.returncode == 0: + return True + return False + + def _fail2ban_ban_ip(self, jail, ip): + result = subprocess.run( + ['/usr/bin/fail2ban-client', 'set', jail, 'banip', ip], + check=True, + capture_output=True, + ) + self._log.debug( + "Adding ban to IP '%s' in jail '%s' result: %s", + ip, + jail, + result.stdout + ) + if result.returncode == 0: + return True + return False + + + def _check_mail_user(self, user): + if user != '': + for mail_user in self.mail_users: + if user in mail_user: + return mail_user + return False + def _get_mail_user(self): + mail_users = [] + try: + conn = mariadb.connect( + host=self.config['db_host'], + port=self.config['db_port'], + user=self.config['db_user'], + password=self.config['db_password'] + ) + cur = conn.cursor() + cur.execute("SELECT email FROM mail.users") + for email in cur: + mail_users.append(email) + except mariadb.Error as error: + self._log.error( + "Error connecting to the database: %s", + error + ) + sys.exit(1) + conn.close() + return mail_users def _init_log(self): ''' Initialize log object ''' @@ -85,6 +209,10 @@ class SmtpdWatcher: @click.option('--log-file', '-l', help="File to store all debug messages.") # @click.option("--dummy","-n", is_flag=True, # help="Don't do anything, just show what would be done.") +@click.option( + '--mail-log-file', '-m', + default='/var/log/mail.log', help='Mail log file to read' +) @click_config_file.configuration_option() def __main__(**kwargs): return SmtpdWatcher(**kwargs)