- config_service.py: read/write jail config via asyncio.gather, global settings, in-process regex validation, log preview via _read_tail_lines - server_service.py: read/write server settings, flush logs - config router: 9 endpoints for jail/global config, regex-test, logpath management, log preview - server router: GET/PUT settings, POST flush-logs - models/config.py expanded with JailConfig, GlobalConfigUpdate, LogPreview* models - 285 tests pass (68 new), ruff clean, mypy clean (44 files) - Frontend: types/config.ts, api/config.ts, hooks/useConfig.ts, ConfigPage.tsx full implementation (Jails accordion editor, Global config, Server settings, Regex Tester with preview) - Fixed pre-existing frontend lint: JSX.Element → React.JSX.Element (10 files), void/promise patterns in useServerStatus + useJails, no-misused-spread in client.ts, eslint.config.ts self-excluded
172 lines
6.2 KiB
Python
172 lines
6.2 KiB
Python
"""Configuration view/edit Pydantic models.
|
|
|
|
Request, response, and domain models for the config router and service.
|
|
"""
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Jail configuration models
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class JailConfig(BaseModel):
|
|
"""Configuration snapshot of a single jail (editable fields)."""
|
|
|
|
model_config = ConfigDict(strict=True)
|
|
|
|
name: str = Field(..., description="Jail name as configured in fail2ban.")
|
|
ban_time: int = Field(..., description="Ban duration in seconds. -1 for permanent.")
|
|
max_retry: int = Field(..., ge=1, description="Number of failures before a ban is issued.")
|
|
find_time: int = Field(..., ge=1, description="Time window (seconds) for counting failures.")
|
|
fail_regex: list[str] = Field(default_factory=list, description="Failure detection regex patterns.")
|
|
ignore_regex: list[str] = Field(default_factory=list, description="Regex patterns that bypass the ban logic.")
|
|
log_paths: list[str] = Field(default_factory=list, description="Monitored log files.")
|
|
date_pattern: str | None = Field(default=None, description="Custom date pattern for log parsing.")
|
|
log_encoding: str = Field(default="UTF-8", description="Log file encoding.")
|
|
backend: str = Field(default="polling", description="Log monitoring backend.")
|
|
actions: list[str] = Field(default_factory=list, description="Names of actions attached to this jail.")
|
|
|
|
|
|
class JailConfigResponse(BaseModel):
|
|
"""Response for ``GET /api/config/jails/{name}``."""
|
|
|
|
model_config = ConfigDict(strict=True)
|
|
|
|
jail: JailConfig
|
|
|
|
|
|
class JailConfigListResponse(BaseModel):
|
|
"""Response for ``GET /api/config/jails``."""
|
|
|
|
model_config = ConfigDict(strict=True)
|
|
|
|
jails: list[JailConfig] = Field(default_factory=list)
|
|
total: int = Field(..., ge=0)
|
|
|
|
|
|
class JailConfigUpdate(BaseModel):
|
|
"""Payload for ``PUT /api/config/jails/{name}``."""
|
|
|
|
model_config = ConfigDict(strict=True)
|
|
|
|
ban_time: int | None = Field(default=None, description="Ban duration in seconds. -1 for permanent.")
|
|
max_retry: int | None = Field(default=None, ge=1)
|
|
find_time: int | None = Field(default=None, ge=1)
|
|
fail_regex: list[str] | None = Field(default=None, description="Failure detection regex patterns.")
|
|
ignore_regex: list[str] | None = Field(default=None)
|
|
date_pattern: str | None = Field(default=None)
|
|
dns_mode: str | None = Field(default=None, description="DNS lookup mode: raw | warn | no.")
|
|
enabled: bool | None = Field(default=None)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Regex tester models
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class RegexTestRequest(BaseModel):
|
|
"""Payload for ``POST /api/config/regex-test``."""
|
|
|
|
model_config = ConfigDict(strict=True)
|
|
|
|
log_line: str = Field(..., description="Sample log line to test against.")
|
|
fail_regex: str = Field(..., description="Regex pattern to match.")
|
|
|
|
|
|
class RegexTestResponse(BaseModel):
|
|
"""Result of a regex test."""
|
|
|
|
model_config = ConfigDict(strict=True)
|
|
|
|
matched: bool = Field(..., description="Whether the pattern matched the log line.")
|
|
groups: list[str] = Field(
|
|
default_factory=list,
|
|
description="Named groups captured by a successful match.",
|
|
)
|
|
error: str | None = Field(
|
|
default=None,
|
|
description="Compilation error message if the regex is invalid.",
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Global config models
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class GlobalConfigResponse(BaseModel):
|
|
"""Response for ``GET /api/config/global``."""
|
|
|
|
model_config = ConfigDict(strict=True)
|
|
|
|
log_level: str
|
|
log_target: str
|
|
db_purge_age: int = Field(..., description="Seconds after which ban records are purged from the fail2ban DB.")
|
|
db_max_matches: int = Field(..., description="Maximum stored log-line matches per ban record.")
|
|
|
|
|
|
class GlobalConfigUpdate(BaseModel):
|
|
"""Payload for ``PUT /api/config/global``."""
|
|
|
|
model_config = ConfigDict(strict=True)
|
|
|
|
log_level: str | None = Field(
|
|
default=None,
|
|
description="Log level: CRITICAL, ERROR, WARNING, NOTICE, INFO, DEBUG.",
|
|
)
|
|
log_target: str | None = Field(
|
|
default=None,
|
|
description="Log target: STDOUT, STDERR, SYSLOG, SYSTEMD-JOURNAL, or a file path.",
|
|
)
|
|
db_purge_age: int | None = Field(default=None, ge=0)
|
|
db_max_matches: int | None = Field(default=None, ge=0)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Log observation / preview models
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class AddLogPathRequest(BaseModel):
|
|
"""Payload for ``POST /api/config/jails/{name}/logpath``."""
|
|
|
|
model_config = ConfigDict(strict=True)
|
|
|
|
log_path: str = Field(..., description="Absolute path to the log file to monitor.")
|
|
tail: bool = Field(
|
|
default=True,
|
|
description="If true, monitor from current end of file (tail). If false, read from the beginning.",
|
|
)
|
|
|
|
|
|
class LogPreviewRequest(BaseModel):
|
|
"""Payload for ``POST /api/config/preview-log``."""
|
|
|
|
model_config = ConfigDict(strict=True)
|
|
|
|
log_path: str = Field(..., description="Absolute path to the log file to preview.")
|
|
fail_regex: str = Field(..., description="Regex pattern to test against log lines.")
|
|
num_lines: int = Field(default=200, ge=1, le=5000, description="Number of lines to read from the end of the file.")
|
|
|
|
|
|
class LogPreviewLine(BaseModel):
|
|
"""A single log line with match information."""
|
|
|
|
model_config = ConfigDict(strict=True)
|
|
|
|
line: str
|
|
matched: bool
|
|
groups: list[str] = Field(default_factory=list)
|
|
|
|
|
|
class LogPreviewResponse(BaseModel):
|
|
"""Response for ``POST /api/config/preview-log``."""
|
|
|
|
model_config = ConfigDict(strict=True)
|
|
|
|
lines: list[LogPreviewLine] = Field(default_factory=list)
|
|
total_lines: int = Field(..., ge=0)
|
|
matched_count: int = Field(..., ge=0)
|
|
regex_error: str | None = Field(default=None, description="Set if the regex failed to compile.")
|