Files
mission-control-v2/backend/tests/test_sicherheit.py
T
HitonabiandClaude Opus 5.5 1c66daad0b Welle 2 · Sicherheits-Markierung: Debian-Sicherheitsupdates, Alpine über secdb, Lücken-Scans aus Arcane
Der Ausführer zählt je Gast, wie viele offene Updates aus einer Sicherheits-Suite kommen („-security“ in apt list),
beim Proxmox-Host ebenso; für Alpine (NPMplus) schickt er die Paketliste, und der Homelab-Teil gleicht sie mit Alpines
Sicherheitsdatenbank ab (im Hintergrund geladen, zwölf Stunden gemerkt, Alpine-Versionsvergleich). Die Update-Tabelle,
die Kacheln und die Übersicht zeigen „· 3 Sicherheit“, die Rückfrage nennt es. Je Docker-Container steht, was Arcanes
Lücken-Scan (Trivy, sonntags 02:00) im Image fand, kritische und hohe. Der Ausführer braucht ausfuehrer-einrichten.sh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 18:02:33 +02:00

128 lines
6.7 KiB
Python

"""Sicherheits-Markierung (Ausbauplan Welle 2, Punkt 11, 25.09.2026): Debian-Suiten „-security“ (Ausführer), Alpine
über die Sicherheitsdatenbank (NPMplus, Alpine 3.24), Lücken-Scans aus Arcane je Container."""
import importlib.util
import json
import time
from pathlib import Path
import pytest
from kern import einstellungen as einstellungen_mod
from services.homelab import arcane, docker, einstellungen, sicherheit
AUSFUEHRER = Path(__file__).resolve().parents[2] / "deploy" / "homelab" / "ausfuehrer.py"
@pytest.fixture(scope="module")
def a():
spec = importlib.util.spec_from_file_location("ausfuehrer_sicherheit", AUSFUEHRER)
modul = importlib.util.module_from_spec(spec)
spec.loader.exec_module(modul)
return modul
def test_alpine_versionen_vergleichen():
t = sicherheit.apk_teile
assert t("1.37.0-r18") < t("1.37.0-r19") < t("1.37.1-r0")
assert t("3.3.2_rc1-r0") < t("3.3.2-r0") < t("3.3.2_p1-r0")
assert t("1.2-r0") < t("1.2a-r0") and t("2.042-r0") > t("2.41-r9")
def test_alpine_sicherheitsupdates_zaehlen():
fixes = {"busybox": ["1.37.0-r19", "1.36.1-r2"], "py3-requests": ["2.32.0-r0"]}
stand = {"alpine": "3.24.2", "pakete": [
{"name": "busybox", "alt": "1.37.0-r18", "neu": "1.37.0-r19"}, # schließt eine Lücke
{"name": "py3-requests", "alt": "2.32.3-r0", "neu": "2.32.4-r0"}, # Fix war schon älter
{"name": "curl", "alt": "8.14.1-r0", "neu": "8.14.1-r1"}]} # ohne Eintrag
assert sicherheit.alpine_sicherheit(stand, fixes) == 1
assert sicherheit.alpine_sicherheit({"pakete": []}) == 0
assert sicherheit.alpine_sicherheit({"anzahl": 3}) is None # Debian-Stand: nicht hier
@pytest.fixture
def daten(tmp_path, monkeypatch):
monkeypatch.setenv("MC_DATEN_DIR", str(tmp_path))
einstellungen_mod.einstellungen.cache_clear()
sicherheit._gemerkt.clear()
yield tmp_path
einstellungen_mod.einstellungen.cache_clear()
sicherheit._gemerkt.clear()
def test_datenbank_laedt_im_hintergrund_und_blockiert_nie(daten, monkeypatch):
geladen: list[str] = []
monkeypatch.setattr(sicherheit, "_laden", lambda zweig: geladen.append(zweig) or sicherheit._laedt.discard(zweig))
monkeypatch.setattr(sicherheit.threading, "Thread", lambda target, args, name, daemon: type(
"T", (), {"start": lambda self: target(*args)})())
assert sicherheit.secdb("3.24") is None and geladen == ["3.24"] # fehlt: nachladen, bis dahin None
(daten / "alpine-secdb-v3.24.json").write_text(json.dumps({"geholt": time.time(), "fixes": {"busybox": ["1-r1"]}}),
encoding="utf-8")
assert sicherheit.secdb("3.24") == {"busybox": ["1-r1"]} and geladen == ["3.24"]
alt = time.time() - sicherheit.NEU_LADEN_S - 60
(daten / "alpine-secdb-v3.24.json").write_text(json.dumps({"geholt": alt, "fixes": {"x": []}}), encoding="utf-8")
assert sicherheit.secdb("3.24") == {"x": []} and geladen == ["3.24", "3.24"] # alt: liefern und nachladen
# --- Ausführer ---------------------------------------------------------------------------------------------------
def test_ausfuehrer_zaehlt_sicherheitsupdates(a, monkeypatch):
monkeypatch.setattr(a, "_im_gast", lambda vmid, befehl, zeitlimit=30: (0, "12\n3\n1790329178\n"))
assert a._os_updates(104, "debian") == {"anzahl": 12, "sicherheit": 3, "listen_stand": 1790329178}
monkeypatch.setattr(a, "_im_gast", lambda vmid, befehl, zeitlimit=30: (
0, "3.24.2\n1790328932\nbusybox-1.37.0-r18 < 1.37.0-r19\n"))
alpine = a._os_updates(101, "alpine")
assert (alpine["anzahl"], alpine["alpine"], alpine["pakete"]) == (
1, "3.24.2", [{"name": "busybox", "alt": "1.37.0-r18", "neu": "1.37.0-r19"}])
liste = ("Listing...\ncurl/stable-security,stable 8.14.1-2+deb13u1 amd64 [upgradable from: 8.14.1-2]\n"
"pve-manager/stable 9.2.20 all [upgradable from: 9.2.19]\n")
assert a.sicherheit_auf_dem_host(liste) == ["curl"]
# --- Homelab-Teil ------------------------------------------------------------------------------------------------
def test_bausteine_tragen_die_markierung(monkeypatch):
from services.homelab import inventar
gast = {"vmid": 104, "art": "lxc", "os_updates": {"anzahl": 12, "sicherheit": 3, "listen_stand": time.time()},
"snapshot_moeglich": True}
stand = inventar._os_baustein(gast, "ct-104", time.time())
assert (stand.sicherheit, stand.kurz) == (3, "12 Pakete") and "(davon 3 Sicherheitsupdates)" in stand.aktion.frage
host = inventar._host_ziel({"version": "9.2.19", "updates": [{"paket": "curl"}, {"paket": "vim"}],
"updates_sicherheit": ["curl"]})
assert host.bausteine[0].sicherheit == 1
ohne = inventar._host_ziel({"version": "9.2.19", "updates": [{"paket": "vim"}]})
assert ohne.bausteine[0].sicherheit is None # älterer Ausführer: unbekannt
def test_docker_mit_luecken_aus_arcane(monkeypatch):
monkeypatch.setattr(einstellungen, "arcane_url", lambda: "http://arcane")
monkeypatch.setattr(arcane, "schluessel", lambda: "schluessel")
monkeypatch.setattr(arcane, "container_liste", lambda url: [
{"id": "a1", "names": ["/nerdquiz"], "imageId": "sha256:q", "state": "running", "labels": {}},
{"id": "a2", "names": ["/arcane"], "imageId": "sha256:a", "state": "running", "labels": {}}])
gefragt: list[tuple] = []
monkeypatch.setattr(arcane, "luecken", lambda url, ids: gefragt.append(ids) or {
"sha256:q": {"kritisch": 2, "hoch": 5, "mittel": 9, "gesamt": 30, "zeit": "2026-09-27T02:04:00Z"}})
zeilen = {c["name"]: c for s in docker.lage()["stacks"] for c in s["container"]}
assert gefragt == [("sha256:a", "sha256:q")]
assert zeilen["nerdquiz"]["luecken"]["kritisch"] == 2 and zeilen["arcane"]["luecken"] is None
def test_arcane_luecken_auslesen(monkeypatch):
monkeypatch.setattr(arcane, "schluessel", lambda: "schluessel")
monkeypatch.setattr(arcane, "umgebungen", lambda url, s=None: [{"id": 0}])
arcane.cache_leeren()
class Antwort:
def raise_for_status(self):
pass
def json(self):
return {"data": {"summaries": {
"sha256:q": {"scanTime": "2026-09-27T02:04:00Z", "summary": {"critical": 2, "high": 5, "total": 30}},
"sha256:n": {"status": "not_scanned", "summary": {}}}}}
monkeypatch.setattr(arcane.httpx, "post", lambda url, headers, json, timeout: Antwort())
assert arcane.luecken("http://arcane", ("sha256:n", "sha256:q")) == {
"sha256:q": {"kritisch": 2, "hoch": 5, "mittel": 0, "gesamt": 30, "zeit": "2026-09-27T02:04:00Z"}}
assert arcane.luecken("http://arcane", ()) == {}