"""Ban history Pydantic models. Request, response, and domain models used by the history router and service. """ from __future__ import annotations from pydantic import BaseModel, ConfigDict, Field from app.models.ban import TimeRange __all__ = [ "HistoryBanItem", "HistoryListResponse", "IpDetailResponse", "IpTimelineEvent", "TimeRange", ] class HistoryBanItem(BaseModel): """A single row in the history ban-list table. Populated from the fail2ban database and optionally enriched with geolocation data. """ model_config = ConfigDict(strict=True) 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(BaseModel): """Paginated history ban-list response.""" model_config = ConfigDict(strict=True) items: list[HistoryBanItem] = Field(default_factory=list) total: int = Field(..., ge=0, description="Total matching records.") page: int = Field(..., ge=1) page_size: int = Field(..., ge=1) # --------------------------------------------------------------------------- # Per-IP timeline # --------------------------------------------------------------------------- class IpTimelineEvent(BaseModel): """A single ban event in a per-IP timeline. Represents one row from the fail2ban ``bans`` table for a specific IP. """ model_config = ConfigDict(strict=True) 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(BaseModel): """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. """ model_config = ConfigDict(strict=True) 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.", )