Files
BanGUI/backend/app/models/history.py

134 lines
4.2 KiB
Python

"""Ban history Pydantic models.
Request, response, and domain models used by the history router and service.
"""
from __future__ import annotations
from pydantic import Field
from app.models.response import BanGuiBaseModel, PaginatedListResponse
from app.models.ban import TimeRange
__all__ = [
"HistoryBanItem",
"HistoryListResponse",
"IpDetailResponse",
"IpTimelineEvent",
"TimeRange",
]
class HistoryBanItem(BanGuiBaseModel):
"""A single row in the history ban-list table.
Populated from the fail2ban database and optionally enriched with
geolocation data.
"""
ip: str = Field(..., description="Banned IP address.")
jail: str = Field(..., description="Jail that issued the ban.")
banned_at: str = Field(..., description="ISO 8601 UTC timestamp of the ban.")
ban_count: int = Field(..., ge=1, description="How many times this IP was banned.")
failures: int = Field(
default=0,
ge=0,
description="Total failure count extracted from the ``data`` column.",
)
matches: list[str] = Field(
default_factory=list,
description="Matched log lines stored in the ``data`` column.",
)
country_code: str | None = Field(
default=None,
description="ISO 3166-1 alpha-2 country code, or ``null`` if unknown.",
)
country_name: str | None = Field(
default=None,
description="Human-readable country name, or ``null`` if unknown.",
)
asn: str | None = Field(
default=None,
description="Autonomous System Number string (e.g. ``'AS3320'``).",
)
org: str | None = Field(
default=None,
description="Organisation name associated with the IP.",
)
class HistoryListResponse(PaginatedListResponse[HistoryBanItem]):
"""Paginated history ban-list response.
Request: ``GET /api/history`` with optional time-range, jail, IP, and
origin filters plus pagination parameters.
Response: Paginated collection of historical ban records with geolocation.
"""
pass
# ---------------------------------------------------------------------------
# Per-IP timeline
# ---------------------------------------------------------------------------
class IpTimelineEvent(BanGuiBaseModel):
"""A single ban event in a per-IP timeline.
Represents one row from the fail2ban ``bans`` table for a specific IP.
"""
jail: str = Field(..., description="Jail that triggered this ban.")
banned_at: str = Field(..., description="ISO 8601 UTC timestamp of the ban.")
ban_count: int = Field(
...,
ge=1,
description="Running ban counter for this IP at the time of this event.",
)
failures: int = Field(
default=0,
ge=0,
description="Failure count at the time of the ban.",
)
matches: list[str] = Field(
default_factory=list,
description="Matched log lines that triggered the ban.",
)
class IpDetailResponse(BanGuiBaseModel):
"""Full historical record for a single IP address.
Contains aggregated totals and a chronological timeline of all ban events
recorded in the fail2ban database for the given IP.
"""
ip: str = Field(..., description="The IP address.")
total_bans: int = Field(..., ge=0, description="Total number of ban records.")
total_failures: int = Field(
...,
ge=0,
description="Sum of all failure counts across all ban events.",
)
last_ban_at: str | None = Field(
default=None,
description="ISO 8601 UTC timestamp of the most recent ban, or ``null``.",
)
country_code: str | None = Field(
default=None,
description="ISO 3166-1 alpha-2 country code, or ``null`` if unknown.",
)
country_name: str | None = Field(
default=None,
description="Human-readable country name, or ``null`` if unknown.",
)
asn: str | None = Field(
default=None,
description="Autonomous System Number string.",
)
org: str | None = Field(
default=None,
description="Organisation name associated with the IP.",
)
timeline: list[IpTimelineEvent] = Field(
default_factory=list,
description="All ban events for this IP, ordered newest-first.",
)