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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
995a0f34ee
commit
1c66daad0b
@@ -0,0 +1,127 @@
|
||||
"""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", ()) == {}
|
||||
Reference in New Issue
Block a user