defmodule ToweropsWeb.Plugs.BruteForceProtection do @moduledoc """ Plug that detects and blocks brute force scanning behavior. ## Detection Strategy Tracks unique 404 paths per IP address using Redis. When an unauthenticated IP hits 5+ unique 404s within 60 seconds, a ban is created/escalated. ## Ban Escalation - 1st offense: 5-minute ban - 2nd offense (within 30 days): 1-hour ban - 3rd+ offense (within 30 days): Permanent ban + Cloudflare WAF push ## Exemptions - Authenticated users (conn.assigns[:current_user] present) - Agent WebSocket connections (/socket/agent - authenticated at channel join) - Whitelisted IPs (checked via BruteForce context) ## Placement Must be placed in the endpoint.ex pipeline AFTER: - RemoteIpLogger (needs remote_ip in Logger metadata) - Static file serving (no need to track static assets) Must be placed BEFORE: - Router (needs to intercept requests before routing) """ import Plug.Conn alias Towerops.Security.BruteForce alias ToweropsWeb.RemoteIp require Logger def init(opts), do: opts def call(conn, _opts) do ip_address = RemoteIp.from_conn(conn) cond do # Skip for authenticated users conn.assigns[:current_user] != nil -> conn # Skip for agent WebSocket connections (authenticated at channel join) String.starts_with?(conn.request_path, "/socket/agent") -> conn # Check if IP is whitelisted BruteForce.check_whitelist(ip_address) -> conn # Check if IP is currently banned true -> case BruteForce.check_ban_status(ip_address) do :allowed -> # Register a callback to track 404s after the response is sent register_before_send(conn, fn response_conn -> if response_conn.status == 404 do Towerops.Security.FourOhFourTracker.record_404(ip_address, conn.request_path) end response_conn end) {:blocked, banned_until} -> Logger.warning("Blocked request from banned IP: #{ip_address}, banned until: #{inspect(banned_until)}") conn |> put_resp_content_type("text/plain") |> send_resp(403, "Access denied") |> halt() end end end end