Commit Graph

49 Commits

Author SHA1 Message Date
9072117db3 ## 28) Login failure delay can enable app-layer DoS 2026-04-29 19:02:00 +02:00
a2129bb9bd Pagination contract is not standardized across endpoints 2026-04-28 21:40:22 +02:00
2e221f6852 Refactor: Move module-level mutable flags to JailServiceState
TASK-004: Replace module-level mutable runtime flags in service layer with
injected state holder, eliminating hidden global state and improving testability
and synchronization boundaries.

Changes:
- Create JailServiceState dataclass in app/utils/runtime_state.py to hold
  backend capability cache and synchronization lock
- Add JailServiceState as a field in RuntimeState (with default_factory)
- Remove module-level _backend_cmd_supported and _backend_cmd_lock from
  jail_service.py
- Refactor _check_backend_cmd_supported() to accept state parameter
- Inject JailServiceState into list_jails() and _fetch_jail_summary() via
  parameters
- Add get_jail_service_state() dependency provider in app/dependencies.py
- Add JailServiceStateDep type alias for router injection
- Update jails router to receive and pass state to service functions
- Update all tests to use jail_service_state fixture and pass state to functions
- Remove duplicate _MAX_PAGE_SIZE constant definition
- Document mutable state management in Backend-Development.md
- Update Architecture.md to describe JailServiceState and state nesting pattern

Benefits:
- Eliminates global mutable state and associated race conditions
- Makes state visible to callers (not hidden in module scope)
- Enables test isolation (each test gets fresh state)
- Prepares codebase for multi-worker deployments (state can be extracted to
  shared backend)
- Synchronization boundaries are now explicit (state.get_backend_cmd_lock())

Compliance:
- All tests pass (17 passed in TestListJails, TestGetJail, TestLockInitialization)
- No ruff linting errors
- Type-safe: JailServiceState properly typed with asyncio.Lock, bool | None

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-27 18:42:52 +02:00
5d24780c63 TASK-028: Add exception logging to fire-and-forget asyncio.create_task()
- Create logged_task() helper in backend/app/utils/async_utils.py to wrap
  fire-and-forget coroutines with exception logging
- Ensures unhandled task exceptions are always logged to structlog instead of
  silently discarded (Python 3.11+ RuntimeWarning)
- Update ban_service.py to use logged_task() for geo_cache.lookup_batch()
  background resolution
- Add comprehensive tests for logged_task() in test_async_utils.py
- Document fire-and-forget task conventions in Backend-Development.md

The logged_task() wrapper catches any exception raised in a background task,
logs it with full traceback context and task name, and never re-raises.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-26 15:17:30 +02:00
d476e9d611 TASK-020: Fix log_target security vulnerability (defense in depth)
**Issue:**
- log_target accepted arbitrary paths, allowing authenticated users to write
  files as root via fail2ban (e.g., /etc/cron.d/bangui-pwned)
- fail2ban runs as root and opens files specified in log_target

**Solution:**
1. **Model layer validation:** Already existed in GlobalConfigUpdate, prevents
   invalid paths before reaching service
2. **Service layer validation:** Added defensive check in update_global_config()
   that validates log_target even if model validation is bypassed
3. **New validation helper:** Added validate_log_target() utility that accepts
   special values (STDOUT, STDERR, SYSLOG) or paths within allowed directories

**Changes:**
- app/utils/path_utils.py: Added validate_log_target() helper
- app/services/config_service.py: Added service-layer validation before
  sending command to fail2ban
- backend/tests: Fixed session_secret length issues in fixtures (min 32 chars)
- backend/tests: Added tests for valid special log targets
- Docs/Backend-Development.md: Documented log_target security requirements

**Test Coverage:**
- Model validation rejects /etc/passwd (existing test)
- Model validation accepts STDOUT, STDERR, SYSLOG special values
- Model validation accepts paths in allowed directories
- Service layer validation tested with special values

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-26 14:23:56 +02:00
667ab674ca Fix SQLite LIKE wildcard escaping in IP filter queries
- Add escape_like() helper to escape % and _ wildcards in LIKE queries
- Update fail2ban_db_repo.get_history_page() to use escaping
- Update history_archive_repo.get_archived_history() to use escaping
- Add ESCAPE clause to all LIKE queries
- Add comprehensive unit tests for escape_like function
- Add integration tests for LIKE wildcard handling
- Document LIKE escaping best practices in Backend-Development.md

Fixes TASK-017: Prevent unintended LIKE matches when IP filter contains
special characters like underscore or percent sign.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-26 14:07:49 +02:00
94bdabe622 TASK-016: Validate delete_log_path query parameter with allowlist
- Extract path validation logic into shared helper function in
  backend/app/utils/path_utils.py (validate_log_path)
- Refactor AddLogPathRequest to use the helper function
- Apply the same validation to DELETE /api/config/jails/{name}/logpath
  endpoint by validating the log_path query parameter
- Return HTTP 422 with descriptive error if validation fails
- Add comprehensive unit tests for path validation
- Update Backend-Development.md with usage examples

This prevents path-traversal attacks on the delete_log_path endpoint
by ensuring all log paths are within allowlisted directories.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-26 14:04:21 +02:00
4ab767e3d4 TASK-009: Mitigate SSRF vulnerability in blocklist URL validation
- Change BlocklistSourceCreate.url from str to AnyHttpUrl (Pydantic type)
  - Rejects non-http schemes (file://, ftp://, etc.) at model boundary

- Add is_private_ip() utility to detect RFC 1918 private ranges:
  - 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 (RFC 1918)
  - 127.0.0.0/8, ::1/128 (loopback)
  - 169.254.0.0/16, fe80::/10 (link-local)
  - IPv6 site-local, multicast, and reserved ranges

- Add async validate_blocklist_url() function:
  - Resolves hostname via DNS using loop.run_in_executor()
  - Rejects if hostname resolves to private/reserved IP
  - Raises ValueError on validation failure

- Integrate validation into service layer:
  - create_source() calls validate_blocklist_url() before persist
  - update_source() conditionally validates if url provided
  - Both raise ValueError on failure

- Update router endpoints with error handling:
  - create_blocklist() and update_blocklist() catch ValueError
  - Return HTTP 400 Bad Request with descriptive error message

- Add comprehensive test coverage (9 new SSRF tests):
  - file://, ftp://, localhost, 127.0.0.1, 192.168.x.x
  - 10.x.x.x, 172.16.x.x, 169.254.x.x (link-local)
  - Valid public URLs (passes validation)
  - All 36 service tests passing

- Update documentation:
  - Features.md: Document URL validation constraints
  - Backend-Development.md: Add SSRF prevention pattern section

Fixes SSRF vulnerability where authenticated users could supply
file://, ftp://, or private IP URLs and the backend would fetch them.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-26 12:57:23 +02:00
ea4c7c2f85 Implement login endpoint rate limiting (TASK-007)
- Add in-memory rate limiter with per-IP deque tracking of attempt timestamps
- Limit login attempts to 5 per 60 seconds per IP, return 429 on excess
- Add Retry-After header to rate limit responses
- Implement IP extraction utility with proxy trust validation (prevent X-Forwarded-For spoofing)
- Integrate rate limiter into auth router and dependencies
- Add 10-second asyncio.sleep on failed login attempts to further slow brute-force
- Add comprehensive tests for rate limiting (9 new tests, all passing)
- Update Features.md to document login rate limiting
- Update Backend-Development.md with rate limiting conventions and design patterns
- Fix test infrastructure issues: update password to meet complexity requirements
- Fix TestValidateSession tests to use Bearer token authentication
- All tests passing: 23 auth tests + full test suite coverage

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-26 12:40:52 +02:00
d982fe3efc TASK-003: Document process-local constraint for RuntimeState and SessionCache
- Add comprehensive docstring to runtime_state.py explaining single-process
  constraint, impacts in multi-worker deployments, and solution approach
- Add comprehensive docstring to session_cache.py explaining process-local
  cache limitation, security implications, and Redis/database alternatives
- Update Architecture.md to clarify session cache is process-local and
  describe single-worker enforcement via TASK-002
- Update Architecture.md runtime state section with detailed explanation of
  per-process state and multi-worker impacts
- Add Backend-Development.md section 13.7.2 documenting session cache
  pluggability pattern with example Redis implementation
- All tests pass; linting passes; type checking has pre-existing errors

This is the short-term fix for TASK-003: enforce single-worker deployment
(TASK-002) and document the constraint clearly. The long-term fix (Redis
backend) is deferred as a follow-up.
2026-04-26 11:43:34 +02:00
ac2028e1c2 Fix: Consolidate divergent _since_unix implementations (T-09)
Consolidate the two divergent implementations of _since_unix from ban_service.py
and history_service.py into a single shared utility function in time_utils.py.

Changes:
- Move _since_unix to app/utils/time_utils.py with consistent time.time() approach
- Move TIME_RANGE_SLACK_SECONDS constant to app/utils/constants.py
- Update ban_service.py to import since_unix from time_utils
- Update history_service.py to import since_unix from time_utils
- Both services now use the same window boundary calculation with 60-second slack
- Add comprehensive tests for the shared since_unix function
- Document timestamp handling rationale in Backend-Development.md

This ensures dashboard and history queries return consistent row counts for the
same time range by using the same timestamp calculation and slack window across
all services.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-25 18:44:59 +02:00
420ea18fd9 Refactor backend services and utilities
- Update service layer implementations
- Improve configuration handling utilities
- Update documentation tasks

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-25 18:39:30 +02:00
83452ffc23 Refactor backend services and jail configuration
- Refactor action_config_service, filter_config_service, jail_config_service, and jail_service
- Add jail_socket utility module for socket communication
- Update test_jail_service with new test cases
- Update architecture and task documentation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-25 18:34:03 +02:00
e57d19fd76 T-05: Remove app.state mutation from _build_app_context
Move session cache initialization from per-request _build_app_context to
startup lifespan handler. The session cache type is now decided once at app
startup based on settings, making _build_app_context pure (read-only).

Changes:
- Move cache initialization logic to new _update_session_cache() in main.py
- Call _update_session_cache() during lifespan startup to initialize cache
- Remove three if/elif/elif branches mutating state.session_cache from _build_app_context
- Add cache swap logic to set_runtime_settings() in runtime_state.py to handle
  runtime settings changes (e.g., setup wizard updates)
- Keep app.state.session_cache initialization in create_app() for test compatibility

This ensures:
- _build_app_context is pure and doesn't mutate app state on each request
- Session cache configuration decisions are centralized at startup
- Settings changes during runtime (via setup wizard) also trigger cache swap
- Cache initialization logic is isolated in one place

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-25 18:23:08 +02:00
fdfd24508f Refactor backend services and routers
- Reorganized dashboard router with improved structure
- Enhanced ban_service with better separation of concerns
- Updated history service with cleaner logic
- Improved constants and configuration handling
- Updated documentation of completed tasks

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-23 16:06:10 +02:00
b634ce876a refactor: Extract fail2ban response utilities into shared module
Consolidate duplicate _ok(), _to_dict(), ensure_list(), and is_not_found_error()
functions from 6 service modules into a single canonical implementation at
backend/app/utils/fail2ban_response.py.

Changes:
- Create fail2ban_response.py with canonical implementations
- Remove local duplicates from: ban_service, jail_service, config_service,
  health_service, server_service, config_file_utils
- Update all imports to use shared module
- Add comprehensive docstrings and examples
- Update Architecture.md and Backend-Development.md documentation

Benefits:
- Single source of truth for response parsing logic
- Eliminates code duplication across service layer
- Improves maintainability and consistency
- Enables centralized bug fixes and improvements

Tests: All 228 service tests passing, no regressions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-23 15:11:21 +02:00
be1d66988f Document RuntimeState concurrency model and mark task 5 complete 2026-04-18 19:56:41 +02:00
7a1cb0c46c Extract health-check crash-detection logic into runtime state helper 2026-04-17 16:58:24 +02:00
74ff4cb4b8 Remove repository import from setup_utils and move password-hash helper to setup service 2026-04-17 15:38:41 +02:00
c21cf82e9e Refactor map color threshold storage into dedicated settings service 2026-04-17 15:13:07 +02:00
328f3575e2 Move Fail2Ban exceptions into central app.exceptions module 2026-04-15 10:22:48 +02:00
a79f5339bc Refactor fail2ban DB path lookup and simplify geo router response 2026-04-15 09:15:50 +02:00
53cdd63b6a Add no-op session cache when session cache is disabled
Use NoOpSessionCache in backend/app/main.py and dynamically switch cache implementation in backend/app/dependencies.py so disabled cache mode remains safe while get_session_cache always returns a valid object.
2026-04-14 12:14:50 +02:00
ec91c1c8b2 Use shared blocking executor in run_blocking
Wire DEFAULT_BLOCKING_EXECUTOR as the default executor in backend/app/utils/async_utils.py, preserving custom executors and marking Task 22 completed in Docs/Tasks.md.
2026-04-14 12:07:35 +02:00
5e4d3fcf12 Remove Mock fallback from runtime_state and add runtime settings regression tests 2026-04-14 10:30:25 +02:00
a564830abb Fix blocklist service injection and centralize session cookie name 2026-04-14 09:21:38 +02:00
5a9d226cca Consolidate fail2ban truthy values into shared constants 2026-04-14 09:03:49 +02:00
72488b14b2 Centralize fail2ban metadata resolution and cache DB path discovery 2026-04-12 19:48:33 +02:00
e271207795 Refactor fail2ban client to use vendored adapter 2026-04-12 19:25:56 +02:00
cd69550053 Standardize async offloading behind shared executor helper 2026-04-11 20:40:08 +02:00
f61d497e4e Refactor backend auth, setup, router, and runtime state handling 2026-04-10 21:00:36 +02:00
3b6e39ddad Separate bootstrap settings from runtime overrides with a dedicated runtime settings manager 2026-04-10 19:31:51 +02:00
1dfc17f4f5 Replace process-local session cache with pluggable session cache backend 2026-04-10 19:22:02 +02:00
2157502670 Eliminate direct app.state access from routers 2026-04-10 19:15:37 +02:00
ff92733f90 Move runtime application state into a dedicated runtime state manager 2026-04-10 19:07:35 +02:00
6b177f1881 Mark async socket handling task done and implement startup cleanup 2026-04-09 22:13:22 +02:00
208f98dc97 Use session_secret for signed auth session tokens 2026-04-09 21:30:08 +02:00
3cc495dfce Remove obsolete utility modules after helper refactor 2026-04-07 20:40:38 +02:00
89ab41cc9e Convert setup guard to startup-driven cache and update tests 2026-04-06 20:38:15 +02:00
3ccfc20c64 Harden fail2ban integration and mark task complete 2026-04-06 20:20:14 +02:00
bf2abda595 chore: commit local changes 2026-03-22 14:24:32 +01:00
a442836c5c refactor: complete Task 2/3 geo decouple + exceptions centralization; mark as done 2026-03-22 14:24:25 +01:00
1c0bac1353 refactor: improve backend type safety and import organization
- Add TYPE_CHECKING guards for runtime-expensive imports (aiohttp, aiosqlite)
- Reorganize imports to follow PEP 8 conventions
- Convert TypeAlias to modern PEP 695 type syntax (where appropriate)
- Use Sequence/Mapping from collections.abc for type hints (covariant)
- Replace string literals with cast() for improved type inference
- Fix casting of Fail2BanResponse and TypedDict patterns
- Add IpLookupResult TypedDict for precise return type annotation
- Reformat overlong lines for readability (120 char limit)
- Add asyncio_mode and filterwarnings to pytest config
- Update test fixtures with improved type hints

This improves mypy type checking and makes type relationships explicit.
2026-03-22 14:24:24 +01:00
ce59a66973 Move conffile_parser from services to utils 2026-03-22 14:24:24 +01:00
bf82e38b6e Fix blocklist-import bantime, unify filter bar, and improve config navigation 2026-03-17 11:31:46 +01:00
57cf93b1e5 Add ensure_jail_configs startup check for required jail config files
On startup BanGUI now verifies that the four fail2ban jail config files
required by its two custom jails (manual-Jail and blocklist-import) are
present in `$fail2ban_config_dir/jail.d`.  Any missing file is created
with the correct default content; existing files are never overwritten.

Files managed:
  - manual-Jail.conf        (enabled=false template)
  - manual-Jail.local       (enabled=true override)
  - blocklist-import.conf   (enabled=false template)
  - blocklist-import.local  (enabled=true override)

The check runs in the lifespan hook immediately after logging is
configured, before the database is opened.
2026-03-16 16:26:39 +01:00
2f2e5a7419 fix: retry, semaphore, reload lock, activation verify, bans_by_jail diagnostics
Stage 1.1-1.3: reload_all include/exclude_jails params already implemented;
  added keyword-arg assertions in router and service tests.

Stage 2.1/6.1: _send_command_sync retry loop (3 attempts, 150ms exp backoff)
  retrying on EAGAIN/ECONNREFUSED/ENOBUFS; immediate raise on all other errors.

Stage 2.2: asyncio.Lock at module level in jail_service.reload_all to
  serialize concurrent reload--all commands.

Stage 3.1: activate_jail re-queries _get_active_jail_names after reload;
  returns active=False with descriptive message if jail did not start.

Stage 4.1/6.2: asyncio.Semaphore (max 10) in Fail2BanClient.send, lazy-
  initialized; logs fail2ban_command_waiting_semaphore at debug when waiting.

Stage 5.1/5.2: unit tests asserting reload_all is called with include_jails
  and exclude_jails; activation verification happy/sad path tests.

Stage 6.3: TestSendCommandSyncRetry (5 cases) + TestFail2BanClientSemaphore
  concurrency test.

Stage 7.1-7.3: _since_unix uses time.time(); bans_by_jail debug logging with
  since_iso; diagnostic warning when total==0 despite table rows; unit test
  verifying the warning fires for stale data.
2026-03-14 11:09:55 +01:00
6e4797d71e feat: config file parser + writer utilities with full test coverage (Tasks 4.1, 4.2)
- Fail2BanConfigParser class: merge order, include directives (before/after),
  variable interpolation %(var)s, split_multiline, ordered_conf_files
- config_writer: write_local_override, remove_local_key, delete_local_file
  with atomic writes (os.replace), per-file threading locks, .local-only guard
- 79 tests in tests/test_utils/ (all passing)
- mypy --strict: 60 source files, 0 errors
- ruff: all checks passed
2026-03-13 19:38:03 +01:00
7392c930d6 feat: Stage 1 — backend and frontend scaffolding
Backend (tasks 1.1, 1.5–1.8):
- pyproject.toml with FastAPI, Pydantic v2, aiosqlite, APScheduler 3.x,
  structlog, bcrypt; ruff + mypy strict configured
- Pydantic Settings (BANGUI_ prefix env vars, fail-fast validation)
- SQLite schema: settings, sessions, blocklist_sources, import_log;
  WAL mode + foreign keys; idempotent init_db()
- FastAPI app factory with lifespan (DB, aiohttp session, scheduler),
  CORS, unhandled-exception handler, GET /api/health
- Fail2BanClient: async Unix-socket wrapper using run_in_executor,
  custom error types, async context manager
- Utility modules: ip_utils, time_utils, constants
- 47 tests; ruff 0 errors; mypy --strict 0 errors

Frontend (tasks 1.2–1.4):
- Vite + React 18 + TypeScript strict; Fluent UI v9; ESLint + Prettier
- Custom brand theme (#0F6CBD, WCAG AA contrast) with light/dark variants
- Typed fetch API client (ApiError, get/post/put/del) + endpoints constants
- tsc --noEmit 0 errors
2026-02-28 21:15:01 +01:00