Add ban management features and update documentation
- Implement ban model, service, and router endpoints in backend - Add ban table component and dashboard integration in frontend - Update ban-related types and API endpoints - Add comprehensive tests for ban service and dashboard router - Update documentation (Features, Tasks, Architecture, Web-Design) - Clean up old fail2ban configuration files - Update Makefile with new commands
This commit is contained in:
@@ -19,8 +19,6 @@ import structlog
|
||||
|
||||
from app.models.ban import (
|
||||
TIME_RANGE_SECONDS,
|
||||
AccessListItem,
|
||||
AccessListResponse,
|
||||
BansByCountryResponse,
|
||||
DashboardBanItem,
|
||||
DashboardBanListResponse,
|
||||
@@ -245,90 +243,6 @@ async def list_bans(
|
||||
)
|
||||
|
||||
|
||||
async def list_accesses(
|
||||
socket_path: str,
|
||||
range_: TimeRange,
|
||||
*,
|
||||
page: int = 1,
|
||||
page_size: int = _DEFAULT_PAGE_SIZE,
|
||||
geo_enricher: Any | None = None,
|
||||
) -> AccessListResponse:
|
||||
"""Return a paginated list of individual access events (matched log lines).
|
||||
|
||||
Each row in the fail2ban ``bans`` table can contain multiple matched log
|
||||
lines in its ``data.matches`` JSON field. This function expands those
|
||||
into individual :class:`~app.models.ban.AccessListItem` objects so callers
|
||||
see each distinct access attempt.
|
||||
|
||||
Args:
|
||||
socket_path: Path to the fail2ban Unix domain socket.
|
||||
range_: Time-range preset.
|
||||
page: 1-based page number (default: ``1``).
|
||||
page_size: Maximum items per page, capped at ``_MAX_PAGE_SIZE``.
|
||||
geo_enricher: Optional async callable ``(ip: str) -> GeoInfo | None``.
|
||||
|
||||
Returns:
|
||||
:class:`~app.models.ban.AccessListResponse` containing the paginated
|
||||
expanded access items and total count.
|
||||
"""
|
||||
since: int = _since_unix(range_)
|
||||
effective_page_size: int = min(page_size, _MAX_PAGE_SIZE)
|
||||
|
||||
db_path: str = await _get_fail2ban_db_path(socket_path)
|
||||
log.info("ban_service_list_accesses", db_path=db_path, since=since, range=range_)
|
||||
|
||||
async with aiosqlite.connect(f"file:{db_path}?mode=ro", uri=True) as f2b_db:
|
||||
f2b_db.row_factory = aiosqlite.Row
|
||||
async with f2b_db.execute(
|
||||
"SELECT jail, ip, timeofban, data "
|
||||
"FROM bans "
|
||||
"WHERE timeofban >= ? "
|
||||
"ORDER BY timeofban DESC",
|
||||
(since,),
|
||||
) as cur:
|
||||
rows = await cur.fetchall()
|
||||
|
||||
# Expand each ban record into its individual matched log lines.
|
||||
all_items: list[AccessListItem] = []
|
||||
for row in rows:
|
||||
jail = str(row["jail"])
|
||||
ip = str(row["ip"])
|
||||
timestamp = _ts_to_iso(int(row["timeofban"]))
|
||||
matches, _ = _parse_data_json(row["data"])
|
||||
|
||||
geo = None
|
||||
if geo_enricher is not None:
|
||||
try:
|
||||
geo = await geo_enricher(ip)
|
||||
except Exception: # noqa: BLE001
|
||||
log.warning("ban_service_geo_lookup_failed", ip=ip)
|
||||
|
||||
for line in matches:
|
||||
all_items.append(
|
||||
AccessListItem(
|
||||
ip=ip,
|
||||
jail=jail,
|
||||
timestamp=timestamp,
|
||||
line=line,
|
||||
country_code=geo.country_code if geo else None,
|
||||
country_name=geo.country_name if geo else None,
|
||||
asn=geo.asn if geo else None,
|
||||
org=geo.org if geo else None,
|
||||
)
|
||||
)
|
||||
|
||||
total: int = len(all_items)
|
||||
offset: int = (page - 1) * effective_page_size
|
||||
page_items: list[AccessListItem] = all_items[offset : offset + effective_page_size]
|
||||
|
||||
return AccessListResponse(
|
||||
items=page_items,
|
||||
total=total,
|
||||
page=page,
|
||||
page_size=effective_page_size,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# bans_by_country
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user