From d780f2ba6cc7741e0d318459a77d9676b1aca92e Mon Sep 17 00:00:00 2001 From: rmanach Date: Sun, 25 May 2025 15:45:32 +0200 Subject: [PATCH] add README --- README.md | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ ufwban.py | 16 ++++++++++++--- 2 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..0c20af8 --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# ufwban + +A little CLI tool (wrapping **ufw**) that read Nginx access logs and block ip based on simple rules. +**THIS IS NOT** a replacement of **fail2ban**, i just do it for fun and to have a simple tool to configure quickly banning undesired IP. + +This a **RUDE** IP deny. It will ban the IP on all machine ports with no ban time, so be careful ! + +## Requirements +* ufw +* Nginx +* Python >= 3.10 + +## Configuration +Create a `conf.json` next to the script. Use the [conf.json.example](./conf.json.example) for sample. + +```json +{ + "rules": { + "codes": [], + "contents": [], + "agents": [] + }, + "whitelist": [] +} +``` +* **codes**: List of unauthorized HTTP codes +* **contents**: string parts that are not not allowed in the request URL (ex: /x00, .json, .php, .env.local, etc...) +* **agents** string parts that are not not allowed in user-agent (ex: bot) +* **whitelist**: List of IP to whitelist (ex: 192.168.1.1) + +## Run +```bash +usage: ufwban [-h] [--dry-run] [--refresh] [--reload] [--live] + +Ban ip from Nginx access logs based on simple rules. + +options: + -h, --help show this help message and exit + --dry-run + --refresh Drop all the deny ip in the UFW table and return + --reload Reload the UFW firewall + --live Read inputs from stdin +``` + +* Batch mode: +```bash +# read all access.log*, parse Nginx log and ban ip +python ufwban.py + +# you can launch a --dry-run mode to see which ip is going to be denied +python ufwban.py --dry-run + +# drop all "DENY IN" ufw rules (be careful) +python ufwban.py --refresh +``` + +* Live mode: +```bash +# Read and parse Nginx logs on each new entry and ban ip +tail -f /var/log/nginx/access.log | python ufwban.py +``` \ No newline at end of file diff --git a/ufwban.py b/ufwban.py index 67410cc..403bf0a 100644 --- a/ufwban.py +++ b/ufwban.py @@ -22,8 +22,11 @@ UFW_CONF = "conf.json" class UFW: + """Wraps ufw binary commands""" + @staticmethod def drop_all(): + """Remove all "DENY IN" rules.""" logging.info("dropping all deny rules...") while (ufw_deny_ips := UFW.list_deny()) and len(ufw_deny_ips): for ip, id_ in ufw_deny_ips.items(): @@ -33,6 +36,7 @@ class UFW: @staticmethod def reload(): + """Force ufw reload.""" logging.info("reloading ufw...") process = subprocess.run(["ufw", "reload"], capture_output=True) @@ -41,6 +45,7 @@ class UFW: @staticmethod def delete_deny_ip(id_: str): + """Delete a rule based on its id.""" logging.info(f"cmd running: ufw delete {id_}") process = subprocess.run( ["ufw", "delete", id_], input=b"y\n", capture_output=True @@ -53,6 +58,7 @@ class UFW: @staticmethod def ban_ip(ip: str): + """Deny an ip for any ports on the machine.""" logging.info(f"cmd running: ufw deny from {ip}") process = subprocess.run(["ufw", "deny", "from", ip], capture_output=True) @@ -61,6 +67,10 @@ class UFW: @staticmethod def list_deny() -> dict[str, str]: + """ + List all the denied ip. + Return a dict mapping the ip with its ufw id. + """ ips = {} cp = subprocess.run(["ufw", "status", "numbered"], capture_output=True) if cp.returncode != 0: @@ -258,13 +268,13 @@ def main(refresh: bool = False, reload: bool = False, dry_run: bool = False): logs = parse_nginx_logs() logs_to_deny = get_logs_to_deny(logs, rules) - if args.refresh and not args.dry_run: + if refresh and not dry_run: UFW.drop_all() return for ip, log in logs_to_deny.items(): print(f"> banning log: {log}") - if not args.dry_run: + if not dry_run: UFW.ban_ip(ip) if reload: @@ -337,7 +347,7 @@ if __name__ == "__main__": exit_code = 1 logging.fatal(f"unexpected error occurred, err={e}") except KeyboardInterrupt: - logging.warning("ok, you just kill me..., bye") + logging.warning("ok, you just kill me... Bye !") logging.info(f"ufwban done in elapsed time: {time.perf_counter() - start:.2f}s") exit(exit_code)