fix: Audit-Findings — Realize-Handler, Thread-Safety, Input-Validierung

- _on_realize implementiert (war nur connected, nicht definiert)
- do_unrealize für as_file() Context-Manager-Cleanup
- threading.Lock für _greetd_sock Race Condition
- TOML-Parsing-Fehler abfangen statt Crash
- last-user Datei: Längen- und Zeichenvalidierung
- detect_locale: non-alpha LANG-Werte abweisen
- exec_cmd Plausibility-Check mit shutil.which
- Exception-Details ins Log statt in die UI
- subprocess.run Timeout für Power-Actions
- Sequence[Path] statt tuple[Path, ...] in get_sessions
- Mock-Server _recvall für fragmentierte Reads
- [behavior]-Config-Sektion entfernt (unimplementiert)
- Design Decisions in CLAUDE.md dokumentiert
This commit is contained in:
2026-03-26 12:56:52 +01:00
parent 8b1608f99d
commit 4cd73a430b
11 changed files with 138 additions and 42 deletions
+8
View File
@@ -35,6 +35,14 @@ class TestLoadConfig:
assert config.background is None
def test_returns_defaults_for_corrupt_toml(self, tmp_path: Path) -> None:
toml_file = tmp_path / "moongreet.toml"
toml_file.write_text("this is not valid [[[ toml !!!")
config = load_config(toml_file)
assert config.background is None
def test_resolves_relative_path_against_config_dir(self, tmp_path: Path) -> None:
toml_file = tmp_path / "moongreet.toml"
toml_file.write_text(
+11 -3
View File
@@ -63,6 +63,13 @@ class TestDetectLocale:
assert result == "en"
def test_rejects_non_alpha_lang(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("LANG", "../../etc")
result = detect_locale()
assert result == "en"
class TestLoadStrings:
"""Tests for loading the correct string table."""
@@ -111,8 +118,9 @@ class TestLoadStrings:
result = strings.faillock_attempts_remaining.format(n=1)
assert "1" in result
def test_connection_error_template(self) -> None:
def test_connection_error_is_generic(self) -> None:
strings = load_strings("en")
result = strings.connection_error.format(error="timeout")
assert "timeout" in result
# Error messages should not contain format placeholders (no info leakage)
assert "{" not in strings.connection_error
assert "{" not in strings.socket_error
+35 -2
View File
@@ -40,16 +40,27 @@ class MockGreetd:
self._thread = threading.Thread(target=self._serve, daemon=True)
self._thread.start()
@staticmethod
def _recvall(conn: socket.socket, n: int) -> bytes:
"""Receive exactly n bytes from a socket, handling fragmented reads."""
buf = bytearray()
while len(buf) < n:
chunk = conn.recv(n - len(buf))
if not chunk:
break
buf.extend(chunk)
return bytes(buf)
def _serve(self) -> None:
conn, _ = self._server.accept()
try:
for response in self._responses:
# Receive a message
header = conn.recv(4)
header = self._recvall(conn, 4)
if len(header) < 4:
break
length = struct.unpack("!I", header)[0]
payload = conn.recv(length)
payload = self._recvall(conn, length)
msg = json.loads(payload.decode("utf-8"))
self._received.append(msg)
@@ -182,6 +193,10 @@ class TestLoginFlow:
class TestFaillockWarning:
"""Tests for the faillock warning message logic."""
def test_no_warning_on_zero_attempts(self) -> None:
strings = load_strings("de")
assert faillock_warning(0, strings) is None
def test_no_warning_on_first_attempt(self) -> None:
strings = load_strings("de")
assert faillock_warning(1, strings) is None
@@ -231,3 +246,21 @@ class TestLastUser:
from moongreet.greeter import GreeterWindow
result = GreeterWindow._load_last_user()
assert result is None
def test_load_last_user_rejects_oversized_username(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
cache_path = tmp_path / "last-user"
cache_path.write_text("a" * 300)
monkeypatch.setattr("moongreet.greeter.LAST_USER_PATH", cache_path)
from moongreet.greeter import GreeterWindow
result = GreeterWindow._load_last_user()
assert result is None
def test_load_last_user_rejects_invalid_characters(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
cache_path = tmp_path / "last-user"
cache_path.write_text("../../etc/passwd")
monkeypatch.setattr("moongreet.greeter.LAST_USER_PATH", cache_path)
from moongreet.greeter import GreeterWindow
result = GreeterWindow._load_last_user()
assert result is None
+3 -3
View File
@@ -6,7 +6,7 @@ from unittest.mock import patch, call
import pytest
from moongreet.power import reboot, shutdown
from moongreet.power import reboot, shutdown, POWER_TIMEOUT
class TestReboot:
@@ -17,7 +17,7 @@ class TestReboot:
reboot()
mock_run.assert_called_once_with(
["loginctl", "reboot"], check=True
["loginctl", "reboot"], check=True, timeout=POWER_TIMEOUT
)
@patch("moongreet.power.subprocess.run")
@@ -36,7 +36,7 @@ class TestShutdown:
shutdown()
mock_run.assert_called_once_with(
["loginctl", "poweroff"], check=True
["loginctl", "poweroff"], check=True, timeout=POWER_TIMEOUT
)
@patch("moongreet.power.subprocess.run")