Files
vulncheck/tests/test_integration_reset.py
T
vulncheck 0bf99857cc fix(sync-health): a reset integration counts as not configured
Resetting Wazuh or Nessus in Settings left the dashboard banner red with
"last run failed", and the scheduled sync kept running and failing.

The reset stores encrypt_value(""), which is non-empty ciphertext, and
configured_sources() read the raw column, so every reset source still
counted as configured. It now reads the decrypted value. That one function
drives the Sync Health state, the scheduler's skip for unconfigured
scanners and the failure-mail skip, so all three follow the reset again.

The existing tests covered the reset and the health state separately; the
new one resets through update_setting and then asks sync_health.
2026-10-26 11:42:05 +01:00

125 lines
5.8 KiB
Python

"""Eine konfigurierte Integration muss wieder auf "not configured" zurueck.
Field report 2026-09-10: keiner der konfigurierten Syncjobs liess sich leeren.
Zwei Gruende, beide unabhaengig:
1. Der Admin loescht alle Felder im Dialog und speichert. Der Merge in
`update_setting` holt fuer jedes leer eingereichte Secret-Feld den
gespeicherten Wert zurueck — gebaut, damit man Host und IDs aendern kann,
ohne das Passwort neu zu tippen. Beim Zuruecksetzen arbeitet genau das
dagegen: das Secret ueberlebt. Und selbst mit leeren Feldern bleibt ein
nicht-leerer JSON-String stehen, den die Oberflaeche als "configured"
liest. Ein PUT mit leerem Wert ist kein gueltiges JSON, laeuft also am
Merge vorbei und loescht die Zeile fachlich.
2. Sync Health blieb auch danach rot. Der Zustand `unconfigured` galt nur,
solange die Quelle noch nie gelaufen war — nach dem Reset lagen die alten
sync_runs-Zeilen noch da, und die Quelle fiel auf `stale` statt grau zu
werden. Die Historie bleibt (das Ledger loescht niemand), nur der Zustand
folgt jetzt der Konfiguration.
Run: venv/bin/python -m pytest tests/test_integration_reset.py
"""
import json
import os
import sys
from datetime import datetime, timedelta
from types import SimpleNamespace
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
os.environ.setdefault("JWT_SECRET_KEY", "test-key-test-key-test-key-test-key")
from cryptography.fernet import Fernet # noqa: E402
os.environ.setdefault("AUTH_PROVIDER_CRYPTO_KEY", Fernet.generate_key().decode())
from sqlalchemy import create_engine # noqa: E402
from sqlalchemy.orm import sessionmaker # noqa: E402
import app.models # noqa: F401,E402 (registers every mapper)
from app.models.base import Base # noqa: E402
from app.models.sync_run import SyncRun # noqa: E402
from app.auth.setting_crypto import read_setting_value # noqa: E402
from app.routers.settings import SettingUpdate, get_setting, update_setting # noqa: E402
ADMIN = SimpleNamespace(id=None) # update_setting needs a user; id None = no actor lookup
from app.services import sync_run_service as svc # noqa: E402
from app.services.intune_service import load_intune_config # noqa: E402
def _db():
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
return sessionmaker(bind=engine)()
FULL = {"tenant_id": "t-1", "client_id": "c-1", "client_secret": "s3cret", "verify_ssl": True}
def test_reset_loescht_config_samt_secret():
db = _db()
update_setting("intune_config", SettingUpdate(value=json.dumps(FULL)), db=db, current_user=ADMIN)
assert load_intune_config(db) is not None
update_setting("intune_config", SettingUpdate(value=""), db=db, current_user=ADMIN)
# Nichts bleibt liegen: weder im Klartext hinter der Verschluesselung ...
assert not (read_setting_value(db, "intune_config") or "")
# ... noch in der Antwort, aus der die Oberflaeche "configured" ableitet.
assert get_setting("intune_config", db=db, current_user=None)["value"] is None
# ... und der Scheduler ueberspringt den Job wieder.
assert load_intune_config(db) is None
def test_alle_felder_leeren_ist_kein_reset():
"""Der Merge ist Absicht — der Reset darf sich nicht darauf verlassen."""
db = _db()
update_setting("intune_config", SettingUpdate(value=json.dumps(FULL)), db=db, current_user=ADMIN)
emptied = {**FULL, "tenant_id": "", "client_id": "", "client_secret": ""}
update_setting("intune_config", SettingUpdate(value=json.dumps(emptied)), db=db, current_user=ADMIN)
stored = json.loads(read_setting_value(db, "intune_config"))
assert stored["client_secret"] == "s3cret" # Secret ueberlebt
assert get_setting("intune_config", db=db, current_user=None)["value"] # Badge bliebe gruen
def test_sync_health_wird_grau_obwohl_laeufe_existieren():
now = datetime.now()
old = SyncRun(source="wazuh", trigger="scheduled", status="completed", stats="{}",
started_at=now - timedelta(days=30), finished_at=now - timedelta(days=30))
by = {h["source"]: h for h in svc.summarize_health(
latest={"wazuh": old}, last_success={"wazuh": old}, configured=set(), now=now)}
assert by["wazuh"]["state"] == "unconfigured"
assert by["wazuh"]["configured"] is False
def test_konfigurierte_quelle_bleibt_bewertet():
"""Gegenprobe: solange die Config steht, entscheidet weiter der Lauf."""
now = datetime.now()
old = SyncRun(source="wazuh", trigger="scheduled", status="completed", stats="{}",
started_at=now - timedelta(days=30), finished_at=now - timedelta(days=30))
by = {h["source"]: h for h in svc.summarize_health(
latest={"wazuh": old}, last_success={"wazuh": old}, configured={"wazuh"}, now=now)}
assert by["wazuh"]["state"] == "stale"
def test_reset_ueber_die_api_macht_sync_health_grau():
"""Field report 2026-10-26: Wazuh und Nessus zurueckgesetzt, Banner blieb rot.
Der Reset speichert `encrypt_value("")` — Chiffretext, nicht leer. Die
beiden Tests oben pruefen Reset und Health getrennt; erst zusammen
sieht man, dass `configured_sources` die rohe Spalte las.
"""
db = _db()
now = datetime.now()
for key in ("wazuh_config", "nessus_config"):
update_setting(key, SettingUpdate(value=json.dumps({"api_url": "https://w", "access_key": "k"})),
db=db, current_user=ADMIN)
update_setting(key, SettingUpdate(value=""), db=db, current_user=ADMIN)
for source in ("wazuh", "nessus"):
db.add(SyncRun(source=source, trigger="scheduled", status="failed", stats="{}",
error_message="configuration missing", started_at=now, finished_at=now))
db.commit()
by = {h["source"]: h for h in svc.sync_health(db)}
assert by["wazuh"]["state"] == "unconfigured"
assert by["nessus"]["state"] == "unconfigured"