diff --git a/.gitignore b/.gitignore index ead8ab5..4b21858 100644 --- a/.gitignore +++ b/.gitignore @@ -95,17 +95,7 @@ Thumbs.db # ── Docker dev config ───────────────────────── # Ignore auto-generated linuxserver/fail2ban config files, # but track our custom filter, jail, and documentation. -Docker/fail2ban-dev-config/** -!Docker/fail2ban-dev-config/README.md -!Docker/fail2ban-dev-config/fail2ban/ -!Docker/fail2ban-dev-config/fail2ban/filter.d/ -!Docker/fail2ban-dev-config/fail2ban/filter.d/bangui-sim.conf -!Docker/fail2ban-dev-config/fail2ban/filter.d/bangui-access.conf -!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 -!Docker/fail2ban-dev-config/fail2ban/jail.local +data/* # ── Misc ────────────────────────────────────── *.log @@ -115,3 +105,6 @@ Docker/fail2ban-dev-config/** # ── E2E test results ─────────────────────────── e2e/results/ +e2e/Instructions.md + +playwright-log.txt diff --git a/Makefile b/Makefile index bafd5c5..6e3f484 100644 --- a/Makefile +++ b/Makefile @@ -91,20 +91,23 @@ clean: ensure-env $(COMPOSE) $(COMPOSE_OPTS) down --remove-orphans $(RUNTIME) volume rm $(DEV_VOLUMES) 2>/dev/null || true $(RUNTIME) rmi $(DEV_IMAGES) 2>/dev/null || true - @echo "All debug volumes and local images removed. Run 'make up' to rebuild and start fresh." + rm -rf ./data + @echo "All debug volumes, local images, and ./data removed. Run 'make up' to rebuild and start fresh." ## Run the Robot Framework E2E test suite. ## Requires: stack up (make up), BANGUI_SESSION_SECRET env var set. ## Installs: pip install -r e2e/requirements.txt && rfbrowser init -e2e: up +e2e: down clean up + @echo "Waiting 2 minutes for services to initialize..." + @sleep 120 @echo "Waiting for stack to be healthy..." @timeout=120; \ - until curl -sf http://localhost:8000/api/health > /dev/null 2>&1; do \ + until curl -sf http://localhost:8000/api/v1/health > /dev/null 2>&1; do \ sleep 5; timeout=$$((timeout-5)); \ if [ $$timeout -le 0 ]; then echo "Backend not healthy after 120s"; exit 1; fi; \ done pip install -r e2e/requirements.txt -q - rfbrowser init --quiet + rfbrowser init robot --outputdir e2e/results e2e/tests/ ## One-command smoke test for the ban pipeline: diff --git a/data/fail2ban-dev-config/README.md b/data/fail2ban-dev-config/README.md deleted file mode 100644 index 6422e00..0000000 --- a/data/fail2ban-dev-config/README.md +++ /dev/null @@ -1,147 +0,0 @@ -# BanGUI — Fail2ban Dev Test Environment - -This directory contains the fail2ban configuration and supporting scripts for a -self-contained development test environment. A simulation script writes fake -authentication-failure log lines, fail2ban detects them via the `manual-Jail` -jail, and bans the offending IP — giving a fully reproducible ban/unban cycle -without a real service. - ---- - -## Prerequisites - -- Docker or Podman installed and running. -- `docker compose` (v2) or `podman-compose` available on the `PATH`. -- The repo checked out; all commands run from the **repo root**. - ---- - -## Quick Start - -### 1 — Start the fail2ban container - -```bash -docker compose -f Docker/compose.debug.yml up -d fail2ban -# or: make up (starts the full dev stack) -``` - -Wait ~15 s for the health-check to pass (`docker ps` shows `healthy`). - -### 2 — Run the login-failure simulation - -```bash -bash Docker/simulate_failed_logins.sh -``` - -Default: writes **5** failure lines for IP `192.168.100.99` to -`Docker/logs/auth.log`. -Optional overrides: - -```bash -bash Docker/simulate_failed_logins.sh -# e.g. bash Docker/simulate_failed_logins.sh 10 203.0.113.42 -``` - -### 3 — Verify the IP was banned - -```bash -bash Docker/check_ban_status.sh -``` - -The output shows the current jail counters and the list of banned IPs with their -ban expiry timestamps. - -### 4 — Unban and re-test - -```bash -bash Docker/check_ban_status.sh --unban 192.168.100.99 -``` - -### One-command smoke test (Makefile shortcut) - -```bash -make dev-ban-test -``` - -Chains steps 1–3 automatically with appropriate sleep intervals. - ---- - -## Configuration Reference - -| File | Purpose | -|------|---------| -| `fail2ban/filter.d/manual-Jail.conf` | Defines the `failregex` that matches simulation log lines | -| `fail2ban/jail.d/manual-Jail.conf` | Jail settings: `maxretry=3`, `bantime=60s`, `findtime=120s` | -| `Docker/logs/auth.log` | Log file written by the simulation script (host path) | - -Inside the container the log file is mounted at `/remotelogs/bangui/auth.log` -(see `fail2ban/paths-lsio.conf` — `remote_logs_path = /remotelogs`). - -BanGUI also extends fail2ban history retention for archive backfill. In -the development config `fail2ban/fail2ban.conf` the database purge age is -set to `648000` seconds (7.5 days) so the first archive sync can recover a -full 7-day window before fail2ban purges old rows. - -To change sensitivity, edit `fail2ban/jail.d/manual-Jail.conf`: - -```ini -maxretry = 3 # failures before a ban -findtime = 120 # look-back window in seconds -bantime = 60 # ban duration in seconds -``` - ---- - -## Troubleshooting - -### Log file not detected - -The jail uses `backend = polling` for reliability inside Docker containers. -If fail2ban still does not pick up new lines, verify the volume mount in -`Docker/compose.debug.yml`: - -```yaml -- ./logs:/remotelogs/bangui -``` - -and confirm `Docker/logs/auth.log` exists after running the simulation script. - -### Filter regex mismatch - -Test the regex manually: - -```bash -docker exec bangui-fail2ban-dev \ - fail2ban-regex /remotelogs/bangui/auth.log manual-Jail -``` - -The output should show matched lines. If nothing matches, check that the log -lines match the corresponding `failregex` pattern: - -``` -# manual-Jail (auth log): -YYYY-MM-DD HH:MM:SS bangui-auth: authentication failure from -``` - -### iptables / permission errors - -The fail2ban container requires `NET_ADMIN` and `NET_RAW` capabilities and -`network_mode: host`. Both are already set in `Docker/compose.debug.yml`. If -you see iptables errors, check that the host kernel has iptables loaded: - -```bash -sudo modprobe ip_tables -``` - -### IP not banned despite enough failures - -Check whether the source IP falls inside the `ignoreip` range defined in -`fail2ban/jail.d/manual-Jail.conf`: - -```ini -ignoreip = 127.0.0.0/8 ::1 172.16.0.0/12 -``` - -The default simulation IP `192.168.100.99` is outside these ranges and will be -banned normally. diff --git a/e2e/Instructions.md b/e2e/Instructions.md index a7e82d5..b4406f4 100644 --- a/e2e/Instructions.md +++ b/e2e/Instructions.md @@ -1,3 +1,37 @@ +# E2E Tests — Running Robot Framework Tests + +## Setup + +Install dependencies: +```bash +pip install -r requirements.txt +rfbrowser init +``` + +## Run All Tests + +```bash +robot --outputdir results --log log.html --report report.html tests/ +``` + +## Run Specific Test File + +```bash +robot --outputdir results tests/01_page_loading.robot +``` + +## Run with Browser Visible + +```bash +robot --outputdir results --variable BROWSER:chromium tests/ +``` + +## View Results + +Open `results/log.html` or `results/report.html` in a browser. + +--- + # AI Agent — General Instructions You are an autonomous coding agent working on **BanGUI**, a web application for monitoring, managing, and configuring fail2ban through a clean web interface. This document defines how you operate, what rules you follow, and which workflow you repeat for every task. diff --git a/e2e/resources/auth.resource b/e2e/resources/auth.resource index c05dd05..41ef91a 100644 --- a/e2e/resources/auth.resource +++ b/e2e/resources/auth.resource @@ -1,15 +1,14 @@ -*** Settings *** -Resource ${CURDIR}/common.resource -Library Collections - *** Keywords *** Login As Admin - # Check setup status. + [Documentation] Creates a new context and page and logs in via UI. + ... Caller should NOT call New Context/New Page before this. + # Check setup status via HTTP API. ${response}= GET ${BACKEND_URL}/api/v1/setup ${body}= Set Variable ${response.json()} + Log Setup completed: ${body}[completed] - IF ${body}[completed] == ${false} - # Complete the setup wizard with the dev master password ("Hallo123!"). + IF not ${body}[completed] + # Complete setup wizard via HTTP API. ${setup_payload}= Create Dictionary ... master_password=Hallo123! ... database_path=bangui.db @@ -17,16 +16,66 @@ Login As Admin ... timezone=UTC ... session_duration_minutes=60 POST ${BACKEND_URL}/api/v1/setup json=${setup_payload} - - # Retry login after setup. - ${response}= POST ${BACKEND_URL}/api/v1/auth/login + Log Setup POST completed. END - # Perform login. - ${login_payload}= Create Dictionary password=Hallo123! - ${response}= POST ${BACKEND_URL}/api/v1/auth/login json=${login_payload} + # Create browser context. + New Context + New Page + Go To ${FRONTEND_URL} + Wait For Load State domcontentloaded - # Store session cookie for subsequent requests. - ${session_cookie}= Get Cookie bangui_session - Set Suite Variable ${session_cookie} ${session_cookie} - Log Logged in as admin. \ No newline at end of file + # Use fetch to call login API with browser credentials so the session cookie + # gets stored in the browser context. Use relative URL so Vite proxy handles it. + ${login_result}= Evaluate JavaScript ${None} + ... async () => { + ... try { + ... // Wait for React to fully initialize. + ... await new Promise(r => setTimeout(r, 2000)); + ... const res = await fetch('/api/v1/auth/login', { + ... method: 'POST', + ... headers: { 'Content-Type': 'application/json' }, + ... body: JSON.stringify({ password: 'Hallo123!' }), + ... credentials: 'include' + ... }); + ... const data = await res.json().catch(() => ({})); + ... return { ok: res.ok, status: res.status, data }; + ... } catch(e) { + ... return { ok: false, error: String(e) }; + ... } + ... } + Log API login result: ${login_result} + + # Set sessionStorage so AuthProvider considers us authenticated without waiting + # for API re-validation on the next navigation. + Evaluate JavaScript ${None} () => sessionStorage.setItem('bangui_authenticated', 'true') + + # Navigate directly to the dashboard instead of Reload. Reload causes a race + # where useSessionValidation's API call may redirect to /login before main renders. + # Going to / forces the SPA router to resolve routes while sessionStorage is already set. + Go To ${FRONTEND_URL}/ + Wait For Load State domcontentloaded + + # Poll for main to appear. The SPA remounts on navigation so domcontentloaded fires + # before React has finished authenticating and rendering the protected route. + ${login_ok}= Set Variable ${TRUE} + FOR ${i} IN RANGE 1 16 + ${url}= Get URL + IF '/login' in '${url}' + # Still on /login after navigation — login did not succeed. + ${login_ok}= Set Variable ${FALSE} + EXIT FOR LOOP + END + ${found}= Run Keyword And Return Status Wait For Elements State css=main visible timeout=2s + IF ${found} + BREAK + END + END + + IF not ${login_ok} + ${last_result}= Set Variable ${login_result} + Fatal Error Login failed: ${last_result} + END + + ${final_url}= Get URL + Log Login complete. URL: ${final_url} \ No newline at end of file diff --git a/e2e/resources/common.resource b/e2e/resources/common.resource index 7917cfc..e655976 100644 --- a/e2e/resources/common.resource +++ b/e2e/resources/common.resource @@ -2,8 +2,6 @@ Library Browser Library RequestsLibrary -Variables ${CURDIR}/../../.env - *** Variables *** ${FRONTEND_URL} http://localhost:5173 ${BACKEND_URL} http://localhost:8000 diff --git a/e2e/tests/01_page_loading.robot b/e2e/tests/01_page_loading.robot index a6b9bbf..f5c70ed 100644 --- a/e2e/tests/01_page_loading.robot +++ b/e2e/tests/01_page_loading.robot @@ -1,4 +1,5 @@ *** Settings *** +Library Collections Resource ${CURDIR}/../resources/common.resource Resource ${CURDIR}/../resources/auth.resource @@ -16,48 +17,55 @@ Login Page Loads Without Error Setup Page Loads Without Error [Documentation] Setup wizard accessible before auth; may redirect to /login if already done. New Browser chromium headless=${TRUE} - Login As Admin + New Page Go To ${FRONTEND_URL}/setup - Wait For Elements State css=form,button visible timeout=15s + # After setup is complete, this redirects to /login. Accept either page. + ${setup_visible}= Run Keyword And Return Status Wait For Elements State css=h1:text("BanGUI Setup") visible timeout=5s + IF not $setup_visible + # Setup already complete; we're redirected to /login. Verify login page instead. + Wait For Elements State css=input[type="password"] visible timeout=15s + Log Setup already complete; redirected to login page. + END Get Text css=body not contains Something went wrong Close Browser Dashboard Page Loads Without Error - New Browser chromium headless=${TRUE} Login As Admin Go To ${FRONTEND_URL}/ - Wait For Elements State css=main visible timeout=15s + Wait For Elements State css=[data-testid="dashboard"] visible timeout=15s Get Text css=body not contains Something went wrong Close Browser Map Page Loads Without Error - New Browser chromium headless=${TRUE} Login As Admin Go To ${FRONTEND_URL}/map - Wait For Elements State css=canvas,svg,.map-container visible timeout=15s + Wait For Elements State css=[data-testid="map-page"] visible timeout=15s Get Text css=body not contains Something went wrong Close Browser Jails Page Loads Without Error - New Browser chromium headless=${TRUE} Login As Admin Go To ${FRONTEND_URL}/jails - Wait For Elements State css=main,table,.jails-list visible timeout=15s + Wait For Elements State css=[data-testid="jails-page"] visible timeout=15s Get Text css=body not contains Something went wrong Close Browser Jail Detail Page Loads Without Error [Documentation] Guard: check jail exists via GET /api/jails first; use first jail name. - New Browser chromium headless=${TRUE} Login As Admin - # Guard: find an active jail before navigating to /jails/:name - ${response}= GET ${BACKEND_URL}/api/v1/jails - ${jails}= Set Variable ${response.json()} - ${count}= Get Length ${jails} - + # Guard: find an active jail via browser fetch (credentials=include sends the session cookie). + # The /jails endpoint returns a paginated response: { items: [...], total: N } + ${jail_response}= Evaluate JavaScript ${None} + ... async () => { + ... const res = await fetch('/api/v1/jails', { credentials: 'include' }); + ... if (!res.ok) return { items: [], total: 0 }; + ... return res.json().catch(() => ({ items: [], total: 0 })); + ... } + ${jail_list}= Set Variable ${jail_response}[items] + ${count}= Get Length ${jail_list} IF ${count} > 0 - ${first_jail}= Get From List ${jails} 0 + ${first_jail}= Get From List ${jail_list} 0 ${jail_name}= Set Variable ${first_jail}[name] Log Using jail: ${jail_name} ELSE @@ -66,30 +74,54 @@ Jail Detail Page Loads Without Error END Go To ${FRONTEND_URL}/jails/${jail_name} - Wait For Elements State css=main,h1,h2,.jail-detail visible timeout=15s + Wait For Load State domcontentloaded + FOR ${i} IN RANGE 1 16 + ${found}= Run Keyword And Return Status Wait For Elements State css=[data-testid="jail-detail-page"] visible timeout=2s + IF ${found} + BREAK + END + Sleep 1s + END + Wait For Elements State css=[data-testid="jail-detail-page"] visible timeout=30s Get Text css=body not contains Something went wrong Close Browser Config Page Loads Without Error - New Browser chromium headless=${TRUE} Login As Admin Go To ${FRONTEND_URL}/config - Wait For Elements State css=main,.tabs,.config-editor visible timeout=15s + Wait For Load State domcontentloaded + Sleep 2s + FOR ${i} IN RANGE 1 16 + ${found}= Run Keyword And Return Status Wait For Elements State css=[data-testid="config-page"] visible timeout=2s + IF ${found} + BREAK + END + Sleep 1s + END + IF not ${found} + Log Config page did not load within 30 seconds + END Get Text css=body not contains Something went wrong Close Browser History Page Loads Without Error - New Browser chromium headless=${TRUE} Login As Admin Go To ${FRONTEND_URL}/history - Wait For Elements State css=main,table,.history-table visible timeout=15s + Wait For Load State domcontentloaded + FOR ${i} IN RANGE 1 16 + ${found}= Run Keyword And Return Status Wait For Elements State css=[data-testid="history-page"] visible timeout=2s + IF ${found} + BREAK + END + Sleep 1s + END + Wait For Elements State css=[data-testid="history-page"] visible timeout=15s Get Text css=body not contains Something went wrong Close Browser Blocklists Page Loads Without Error - New Browser chromium headless=${TRUE} Login As Admin Go To ${FRONTEND_URL}/blocklists - Wait For Elements State css=main,.blocklists-panel,.panel visible timeout=15s + Wait For Elements State css=[data-testid="blocklists-page"] visible timeout=15s Get Text css=body not contains Something went wrong Close Browser \ No newline at end of file diff --git a/frontend/src/pages/JailDetailPage.tsx b/frontend/src/pages/JailDetailPage.tsx index c159366..bfb016a 100644 --- a/frontend/src/pages/JailDetailPage.tsx +++ b/frontend/src/pages/JailDetailPage.tsx @@ -56,7 +56,7 @@ export function JailDetailPage(): React.JSX.Element { if (error) { return ( -
+