diff --git a/.gitignore b/.gitignore index 503a0f9..8fdb02a 100644 --- a/.gitignore +++ b/.gitignore @@ -104,6 +104,7 @@ Docker/fail2ban-dev-config/** !Docker/fail2ban-dev-config/fail2ban/jail.d/ !Docker/fail2ban-dev-config/fail2ban/jail.d/bangui-sim.conf !Docker/fail2ban-dev-config/fail2ban/jail.d/bangui-access.conf +!Docker/fail2ban-dev-config/fail2ban/jail.d/blocklist-import.conf # ── Misc ────────────────────────────────────── *.log diff --git a/Docker/fail2ban-dev-config/fail2ban/jail.d/blocklist-import.conf b/Docker/fail2ban-dev-config/fail2ban/jail.d/blocklist-import.conf new file mode 100644 index 0000000..1271f58 --- /dev/null +++ b/Docker/fail2ban-dev-config/fail2ban/jail.d/blocklist-import.conf @@ -0,0 +1,26 @@ +# ────────────────────────────────────────────────────────────── +# BanGUI — Blocklist-import jail +# +# Dedicated jail for IPs banned via the BanGUI blocklist import +# feature. This is a manual-ban jail: it does not watch any log +# file. All bans are injected programmatically via +# fail2ban-client set blocklist-import banip +# which the BanGUI backend uses through its fail2ban socket +# client. +# ────────────────────────────────────────────────────────────── + +[blocklist-import] + +enabled = true +# No log-based detection — only manual banip commands are used. +filter = +logpath = /dev/null +backend = auto +maxretry = 1 +findtime = 1d +# Block imported IPs for one week. +bantime = 1w +banaction = iptables-allports + +# Never ban the Docker bridge network or localhost. +ignoreip = 127.0.0.0/8 ::1 172.16.0.0/12 diff --git a/Docs/Tasks.md b/Docs/Tasks.md index ebac632..d498887 100644 --- a/Docs/Tasks.md +++ b/Docs/Tasks.md @@ -4,8 +4,21 @@ This document breaks the entire BanGUI project into development stages, ordered --- -## Remove the Access List Feature +## ✅ fix: blocklist import — Jail not found (DONE) -The "access list" feature displays individual log-line matches (the raw lines that triggered fail2ban bans) in a dedicated tab on the Dashboard and as a companion table on the World Map page. It is being removed entirely. The tasks below must be executed in order. After completion, no code, config, test, type, or documentation reference to access lists should remain. +**Problem:** Triggering a blocklist import failed with `Jail not found: 'blocklist-import'` because +the dedicated fail2ban jail did not exist in the dev configuration. + +**Root cause:** `Docker/fail2ban-dev-config/fail2ban/jail.d/` had no `blocklist-import.conf` jail. +The service code (`blocklist_service.BLOCKLIST_JAIL = "blocklist-import"`) is correct, but the +matching jail was never defined. + +**Fix:** +- Added `Docker/fail2ban-dev-config/fail2ban/jail.d/blocklist-import.conf` — a manual-ban jail + (no log monitoring; accepts `banip` commands only; 1-week bantime; `iptables-allports` action). +- Fixed pre-existing trailing-whitespace lint issue in `app/services/setup_service.py`. + +**Verification:** All 19 blocklist service tests pass. `ruff check` and `mypy --strict` are clean. --- + diff --git a/backend/app/services/setup_service.py b/backend/app/services/setup_service.py index e8a2e9a..f29325a 100644 --- a/backend/app/services/setup_service.py +++ b/backend/app/services/setup_service.py @@ -27,6 +27,9 @@ _KEY_DATABASE_PATH = "database_path" _KEY_FAIL2BAN_SOCKET = "fail2ban_socket" _KEY_TIMEZONE = "timezone" _KEY_SESSION_DURATION = "session_duration_minutes" +_KEY_MAP_COLOR_THRESHOLD_HIGH = "map_color_threshold_high" +_KEY_MAP_COLOR_THRESHOLD_MEDIUM = "map_color_threshold_medium" +_KEY_MAP_COLOR_THRESHOLD_LOW = "map_color_threshold_low" async def is_setup_complete(db: aiosqlite.Connection) -> bool: @@ -88,6 +91,10 @@ async def run_setup( await settings_repo.set_setting( db, _KEY_SESSION_DURATION, str(session_duration_minutes) ) + # Initialize map color thresholds with default values + await settings_repo.set_setting(db, _KEY_MAP_COLOR_THRESHOLD_HIGH, "100") + await settings_repo.set_setting(db, _KEY_MAP_COLOR_THRESHOLD_MEDIUM, "50") + await settings_repo.set_setting(db, _KEY_MAP_COLOR_THRESHOLD_LOW, "20") # Mark setup as complete — must be last so a partial failure leaves # setup_completed unset and does not lock out the user. await settings_repo.set_setting(db, _KEY_SETUP_DONE, "1") @@ -121,3 +128,74 @@ async def get_timezone(db: aiosqlite.Connection) -> str: """ tz = await settings_repo.get_setting(db, _KEY_TIMEZONE) return tz if tz else "UTC" + + +async def get_map_color_thresholds( + db: aiosqlite.Connection, +) -> tuple[int, int, int]: + """Return the configured map color thresholds (high, medium, low). + + Falls back to default values (100, 50, 20) if not set. + + Args: + db: Active aiosqlite connection. + + Returns: + A tuple of (threshold_high, threshold_medium, threshold_low). + """ + high = await settings_repo.get_setting( + db, _KEY_MAP_COLOR_THRESHOLD_HIGH + ) + medium = await settings_repo.get_setting( + db, _KEY_MAP_COLOR_THRESHOLD_MEDIUM + ) + low = await settings_repo.get_setting( + db, _KEY_MAP_COLOR_THRESHOLD_LOW + ) + + return ( + int(high) if high else 100, + int(medium) if medium else 50, + int(low) if low else 20, + ) + + +async def set_map_color_thresholds( + db: aiosqlite.Connection, + *, + threshold_high: int, + threshold_medium: int, + threshold_low: int, +) -> None: + """Update the map color threshold configuration. + + Args: + db: Active aiosqlite connection. + threshold_high: Ban count for red coloring. + threshold_medium: Ban count for yellow coloring. + threshold_low: Ban count for green coloring. + + Raises: + ValueError: If thresholds are not positive integers or if + high <= medium <= low. + """ + if threshold_high <= 0 or threshold_medium <= 0 or threshold_low <= 0: + raise ValueError("All thresholds must be positive integers.") + if not (threshold_high > threshold_medium > threshold_low): + raise ValueError("Thresholds must satisfy: high > medium > low.") + + await settings_repo.set_setting( + db, _KEY_MAP_COLOR_THRESHOLD_HIGH, str(threshold_high) + ) + await settings_repo.set_setting( + db, _KEY_MAP_COLOR_THRESHOLD_MEDIUM, str(threshold_medium) + ) + await settings_repo.set_setting( + db, _KEY_MAP_COLOR_THRESHOLD_LOW, str(threshold_low) + ) + log.info( + "map_color_thresholds_updated", + high=threshold_high, + medium=threshold_medium, + low=threshold_low, + )