towerops/lib/towerops/security/four_oh_four_tracker.ex
Graham McIntire c9f154f8cd
fix 5 failing tests
- increase timing threshold in session activity test (flaky at 50ms)
- show org name instead of agent ID in admin agents list
- fix agent live tests wiping auth session with init_test_session
- add resilient error handling in FourOhFourTracker for missing redis
2026-02-11 12:21:06 -06:00

74 lines
1.9 KiB
Elixir

defmodule Towerops.Security.FourOhFourTracker do
@moduledoc """
Tracks unique 404 paths per IP address using Redis.
Uses Redis SET data structure to track unique paths with 60-second TTL.
When an IP hits 5+ unique 404s, a ban is created/escalated.
"""
alias Towerops.Security.BruteForce
require Logger
@threshold 5
@window_seconds 60
@doc """
Records a 404 path for an IP address.
Stores the path in a Redis SET with 60-second TTL.
Triggers ban creation/escalation if threshold (5) is reached.
"""
def record_404(ip_address, path) do
key = redis_key(ip_address)
conn = Towerops.Redix
try do
# Add path to set
Redix.command(conn, ["SADD", key, path])
# Set TTL if this is the first path
Redix.command(conn, ["EXPIRE", key, @window_seconds])
# Get unique count
case Redix.command(conn, ["SCARD", key]) do
{:ok, count} when count >= @threshold ->
Logger.warning("IP #{ip_address} hit threshold with #{count} unique 404s, triggering ban")
BruteForce.create_or_escalate_ban(ip_address)
:threshold_reached
{:ok, count} ->
Logger.debug("IP #{ip_address} has #{count}/#{@threshold} unique 404s")
:ok
{:error, reason} ->
Logger.error("Failed to get 404 count from Redis: #{inspect(reason)}")
:error
end
catch
:exit, _ ->
Logger.error("Redis unavailable for 404 tracking")
:error
end
end
@doc """
Gets the count of unique 404 paths for an IP address.
"""
def get_count(ip_address) do
key = redis_key(ip_address)
conn = Towerops.Redix
try do
case Redix.command(conn, ["SCARD", key]) do
{:ok, count} -> count
{:error, _} -> 0
end
catch
:exit, _ -> 0
end
end
defp redis_key(ip_address) do
"404_scan:#{ip_address}"
end
end