diff --git a/backend/app/services/config_service.py b/backend/app/services/config_service.py index 6f7998d..6e8e86c 100644 --- a/backend/app/services/config_service.py +++ b/backend/app/services/config_service.py @@ -15,7 +15,6 @@ from __future__ import annotations import asyncio import contextlib import re -from collections.abc import Awaitable, Callable from pathlib import Path from typing import TYPE_CHECKING, TypeVar, cast @@ -24,8 +23,12 @@ import structlog from app.utils.fail2ban_client import Fail2BanCommand, Fail2BanResponse, Fail2BanToken if TYPE_CHECKING: + from collections.abc import Awaitable, Callable + import aiosqlite +from app import __version__ +from app.exceptions import ConfigOperationError, ConfigValidationError, JailNotFoundError from app.models.config import ( AddLogPathRequest, BantimeEscalation, @@ -44,11 +47,13 @@ from app.models.config import ( RegexTestResponse, ServiceStatusResponse, ) -from app.exceptions import ConfigOperationError, ConfigValidationError, JailNotFoundError from app.utils.fail2ban_client import Fail2BanClient -from app.utils.log_utils import preview_log as util_preview_log, test_regex as util_test_regex +from app.utils.log_utils import preview_log as util_preview_log +from app.utils.log_utils import test_regex as util_test_regex from app.utils.setup_utils import ( get_map_color_thresholds as util_get_map_color_thresholds, +) +from app.utils.setup_utils import ( set_map_color_thresholds as util_set_map_color_thresholds, ) @@ -815,6 +820,7 @@ async def get_service_status( return ServiceStatusResponse( online=server_status.online, version=server_status.version, + bangui_version=__version__, jail_count=server_status.active_jails, total_bans=server_status.total_bans, total_failures=server_status.total_failures, diff --git a/backend/app/services/filter_config_service.py b/backend/app/services/filter_config_service.py index ba5e1c5..4ad2cac 100644 --- a/backend/app/services/filter_config_service.py +++ b/backend/app/services/filter_config_service.py @@ -17,16 +17,21 @@ from pathlib import Path import structlog +from app.exceptions import FilterInvalidRegexError from app.models.config import ( + AssignFilterRequest, FilterConfig, FilterConfigUpdate, FilterCreateRequest, FilterListResponse, FilterUpdateRequest, - AssignFilterRequest, ) -from app.exceptions import FilterInvalidRegexError, JailNotFoundError +from app.services.config_file_service import _TRUE_VALUES, ConfigWriteError, JailNotFoundInConfigError from app.utils import conffile_parser +from app.utils.config_file_utils import ( + _get_active_jail_names, + _parse_jails_sync, +) from app.utils.jail_utils import reload_jails log: structlog.stdlib.BoundLogger = structlog.get_logger() @@ -90,6 +95,7 @@ class JailNameError(Exception): """Raised when a jail name contains invalid characters.""" +_SAFE_FILTER_NAME_RE: re.Pattern[str] = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$") _SAFE_JAIL_NAME_RE: re.Pattern[str] = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$") diff --git a/backend/app/services/jail_config_service.py b/backend/app/services/jail_config_service.py index cc8c2e4..55c1d79 100644 --- a/backend/app/services/jail_config_service.py +++ b/backend/app/services/jail_config_service.py @@ -26,22 +26,17 @@ from app.models.config import ( InactiveJail, InactiveJailListResponse, JailActivationResponse, - JailValidationIssue, JailValidationResult, RollbackResponse, ) from app.utils.config_file_utils import ( _build_inactive_jail, - _ordered_config_files, + _get_active_jail_names, _parse_jails_sync, _validate_jail_config_sync, ) +from app.utils.fail2ban_client import Fail2BanClient from app.utils.jail_utils import reload_jails -from app.utils.fail2ban_client import ( - Fail2BanClient, - Fail2BanConnectionError, - Fail2BanResponse, -) log: structlog.stdlib.BoundLogger = structlog.get_logger() diff --git a/backend/tests/test_services/test_config_service.py b/backend/tests/test_services/test_config_service.py index 27d80d9..167d9d0 100644 --- a/backend/tests/test_services/test_config_service.py +++ b/backend/tests/test_services/test_config_service.py @@ -2,6 +2,7 @@ from __future__ import annotations +from pathlib import Path from typing import Any from unittest.mock import AsyncMock, patch @@ -748,8 +749,11 @@ class TestGetServiceStatus: probe_fn=AsyncMock(return_value=online_status), ) + from app import __version__ + assert result.online is True assert result.version == "1.0.0" + assert result.bangui_version == __version__ assert result.jail_count == 2 assert result.total_bans == 5 assert result.total_failures == 3 @@ -771,3 +775,62 @@ class TestGetServiceStatus: assert result.jail_count == 0 assert result.log_level == "UNKNOWN" assert result.log_target == "UNKNOWN" + + +@pytest.mark.asyncio +class TestConfigModuleIntegration: + async def test_jail_config_service_list_inactive_jails_uses_imports(self, tmp_path: Any) -> None: + from app.services.jail_config_service import list_inactive_jails + + # Arrange: fake parse_jails output with one active and one inactive + def fake_parse_jails_sync(path: Path) -> tuple[dict[str, dict[str, str]], dict[str, str]]: + return ( + { + "sshd": { + "enabled": "true", + "filter": "sshd", + "logpath": "/var/log/auth.log", + }, + "apache-auth": { + "enabled": "false", + "filter": "apache-auth", + "logpath": "/var/log/apache2/error.log", + }, + }, + { + "sshd": str(path / "jail.conf"), + "apache-auth": str(path / "jail.conf"), + }, + ) + + with patch( + "app.services.jail_config_service._parse_jails_sync", + new=fake_parse_jails_sync, + ), patch( + "app.services.jail_config_service._get_active_jail_names", + new=AsyncMock(return_value={"sshd"}), + ): + result = await list_inactive_jails(str(tmp_path), "/fake.sock") + + names = {j.name for j in result.jails} + assert "apache-auth" in names + assert "sshd" not in names + + async def test_filter_config_service_list_filters_uses_imports(self, tmp_path: Any) -> None: + from app.services.filter_config_service import list_filters + + # Arrange minimal filter and jail config files + filter_d = tmp_path / "filter.d" + filter_d.mkdir(parents=True) + (filter_d / "sshd.conf").write_text("[Definition]\nfailregex = ^%(__prefix_line)s.*$\n") + (tmp_path / "jail.conf").write_text("[sshd]\nfilter = sshd\nenabled = true\n") + + with patch( + "app.services.filter_config_service._get_active_jail_names", + new=AsyncMock(return_value={"sshd"}), + ): + result = await list_filters(str(tmp_path), "/fake.sock") + + assert result.total == 1 + assert result.filters[0].name == "sshd" + assert result.filters[0].active is True