#!/usr/bin/env python3 """Fail2ban port scan detection and management agent.""" import json import os import re import subprocess import sys from collections import Counter from datetime import datetime FAIL2BAN_CLIENT = os.environ.get("FAIL2BAN_CLIENT", "fail2ban-client") FAIL2BAN_LOG = os.environ.get("FAIL2BAN_LOG", "/var/log/fail2ban.log") AUTH_LOG = os.environ.get("AUTH_LOG", "/var/log/auth.log") def check_fail2ban_status(): """Check Fail2ban service status and active jails.""" try: result = subprocess.run( [FAIL2BAN_CLIENT, "status"], capture_output=True, text=True, timeout=10 ) if result.returncode != 0: return {"running": False, "error": result.stderr.strip()} jails = [] for line in result.stdout.splitlines(): if "Jail list" in line: jail_str = line.split(":", 1)[1].strip() jails = [j.strip() for j in jail_str.split(",") if j.strip()] return {"running": True, "jails": jails, "jail_count": len(jails)} except FileNotFoundError: return {"running": False, "error": "fail2ban-client not found"} def get_jail_status(jail_name): """Get detailed status of a specific jail.""" try: result = subprocess.run( [FAIL2BAN_CLIENT, "status", jail_name], capture_output=True, text=True, timeout=10 ) if result.returncode != 0: return {"error": result.stderr.strip()} info = {"jail": jail_name} for line in result.stdout.splitlines(): line = line.strip() if "Currently failed" in line: info["currently_failed"] = int(line.split(":", 1)[1].strip()) elif "Total failed" in line: info["total_failed"] = int(line.split(":", 1)[1].strip()) elif "Currently banned" in line: info["currently_banned"] = int(line.split(":", 1)[1].strip()) elif "Total banned" in line: info["total_banned"] = int(line.split(":", 1)[1].strip()) elif "Banned IP list" in line: ips = line.split(":", 1)[1].strip() info["banned_ips"] = [ip.strip() for ip in ips.split() if ip.strip()] return info except Exception as e: return {"error": str(e)} def ban_ip(ip_address, jail_name="sshd"): """Manually ban an IP address in a specific jail.""" try: result = subprocess.run( [FAIL2BAN_CLIENT, "set", jail_name, "banip", ip_address], capture_output=True, text=True, timeout=10 ) return { "action": "ban", "ip": ip_address, "jail": jail_name, "success": result.returncode == 0, "timestamp": datetime.utcnow().isoformat() + "Z", } except Exception as e: return {"action": "ban", "success": False, "error": str(e)} def unban_ip(ip_address, jail_name="sshd"): """Unban an IP address from a specific jail.""" try: result = subprocess.run( [FAIL2BAN_CLIENT, "set", jail_name, "unbanip", ip_address], capture_output=True, text=True, timeout=10 ) return { "action": "unban", "ip": ip_address, "jail": jail_name, "success": result.returncode == 0, "timestamp": datetime.utcnow().isoformat() + "Z", } except Exception as e: return {"action": "unban", "success": False, "error": str(e)} def parse_fail2ban_log(log_path=None, limit=5000): """Parse Fail2ban log for ban/unban statistics.""" log_path = log_path or FAIL2BAN_LOG if not os.path.exists(log_path): return {"error": f"Log not found: {log_path}"} bans = Counter() unbans = Counter() jails = Counter() recent_bans = [] with open(log_path, "r") as f: for i, line in enumerate(f): if i > limit: break if "Ban " in line: ip_match = re.search(r'Ban\s+(\d+\.\d+\.\d+\.\d+)', line) jail_match = re.search(r'\[([^\]]+)\]', line) if ip_match: ip = ip_match.group(1) jail = jail_match.group(1) if jail_match else "unknown" bans[ip] += 1 jails[jail] += 1 recent_bans.append({"ip": ip, "jail": jail, "line": line.strip()[:200]}) elif "Unban " in line: ip_match = re.search(r'Unban\s+(\d+\.\d+\.\d+\.\d+)', line) if ip_match: unbans[ip_match.group(1)] += 1 return { "total_bans": sum(bans.values()), "total_unbans": sum(unbans.values()), "unique_banned_ips": len(bans), "top_banned_ips": bans.most_common(20), "bans_by_jail": dict(jails), "recent_bans": recent_bans[-20:], } def parse_auth_log_ssh(log_path=None): """Parse auth.log for SSH brute force patterns.""" log_path = log_path or AUTH_LOG if not os.path.exists(log_path): return {"error": f"Auth log not found: {log_path}"} failed_ips = Counter() failed_users = Counter() success_ips = Counter() with open(log_path, "r") as f: for line in f: if "Failed password" in line or "Failed publickey" in line: ip_match = re.search(r'from\s+(\d+\.\d+\.\d+\.\d+)', line) user_match = re.search(r'for\s+(?:invalid\s+user\s+)?(\S+)', line) if ip_match: failed_ips[ip_match.group(1)] += 1 if user_match: failed_users[user_match.group(1)] += 1 elif "Accepted password" in line or "Accepted publickey" in line: ip_match = re.search(r'from\s+(\d+\.\d+\.\d+\.\d+)', line) if ip_match: success_ips[ip_match.group(1)] += 1 return { "total_failed": sum(failed_ips.values()), "unique_failed_ips": len(failed_ips), "top_failed_ips": failed_ips.most_common(20), "top_targeted_users": failed_users.most_common(20), "successful_logins": success_ips.most_common(10), } def detect_port_scan_from_logs(log_path=None): """Detect port scanning patterns from system logs.""" log_path = log_path or os.environ.get("SYSLOG_PATH", "/var/log/syslog") if not os.path.exists(log_path): return {"error": f"Syslog not found: {log_path}"} scanners = Counter() with open(log_path, "r") as f: for line in f: if "UFW BLOCK" in line or "iptables" in line.lower(): ip_match = re.search(r'SRC=(\d+\.\d+\.\d+\.\d+)', line) if ip_match: scanners[ip_match.group(1)] += 1 port_scanners = {ip: count for ip, count in scanners.items() if count > 20} return { "potential_scanners": len(port_scanners), "scanner_ips": sorted(port_scanners.items(), key=lambda x: x[1], reverse=True)[:20], } def generate_report(): """Generate comprehensive Fail2ban security report.""" return { "timestamp": datetime.utcnow().isoformat() + "Z", "service_status": check_fail2ban_status(), "ban_statistics": parse_fail2ban_log(), "ssh_analysis": parse_auth_log_ssh(), "port_scan_detection": detect_port_scan_from_logs(), } if __name__ == "__main__": action = sys.argv[1] if len(sys.argv) > 1 else "report" if action == "report": print(json.dumps(generate_report(), indent=2, default=str)) elif action == "status": print(json.dumps(check_fail2ban_status(), indent=2)) elif action == "jail" and len(sys.argv) > 2: print(json.dumps(get_jail_status(sys.argv[2]), indent=2)) elif action == "ban" and len(sys.argv) > 2: jail = sys.argv[3] if len(sys.argv) > 3 else "sshd" print(json.dumps(ban_ip(sys.argv[2], jail), indent=2)) elif action == "unban" and len(sys.argv) > 2: jail = sys.argv[3] if len(sys.argv) > 3 else "sshd" print(json.dumps(unban_ip(sys.argv[2], jail), indent=2)) elif action == "bans": print(json.dumps(parse_fail2ban_log(), indent=2)) elif action == "ssh": print(json.dumps(parse_auth_log_ssh(), indent=2)) else: print("Usage: agent.py [report|status|jail |ban [jail]|unban [jail]|bans|ssh]")