feat: check-assets ↔ KI gekoppelt — byte-getauschte Bilder neu bewerten
Da bekannte Bilder aus Effizienzgründen nicht jeden Scan neu geladen werden (url_hashes), prüfte die KI einen späteren Byte-Tausch eines bekannten Bildes nicht von selbst erneut. Lücke geschlossen: Erkennt die Datei-Prüfung (check-assets) eine geänderte Bilddatei, wird deren URL über BaselineManager.invalidate_ai_url_hashes aus der Hash-Erinnerung entfernt → nächster Scan lädt das Bild neu, neue Bytes = neuer Hash = Cache-Miss = die KI bewertet den Inhalt neu. Einhängung in cmd_scan (vor run_ai_analysis → Neubewertung noch im selben Scan) und cmd_check_assets. Integritäts- und Inhaltsprüfung greifen so ineinander. Tests: 260 grün (+2: invalidate entfernt nur die genannten URLs; geändertes Bild wird nach Invalidierung neu geladen und beanstandet). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
4db5817013
commit
78dc79ceab
4 changed files with 80 additions and 2 deletions
|
|
@ -1032,8 +1032,12 @@ einer ansonsten stabilen Seite). Deshalb gilt:
|
|||
akzeptiert. Da die Quittung am **Byte-Hash** hängt, wird ein später ausgetauschtes Bild
|
||||
(andere Bytes → neuer Hash) automatisch neu geprüft und nicht stillschweigend mitakzeptiert.
|
||||
|
||||
Hinweis zur Byte-Änderung bekannter Bilder auf stabilen Seiten: Diese deckt zusätzlich die
|
||||
Datei-Prüfung (`check-assets`) ab — sie schlägt an, wenn sich der Inhalt einer bekannten Datei ändert.
|
||||
**Byte-Tausch bekannter Bilder (z. B. ein bestehendes Logo wird heimlich gegen ein strafbares
|
||||
ausgetauscht):** Bekannte Bilder werden aus Effizienzgründen nicht bei jedem Scan neu geladen
|
||||
(URL→Byte-Hash gemerkt). Damit ein Austausch trotzdem nicht durchrutscht, ist die Datei-Prüfung
|
||||
(`check-assets`) an die KI **gekoppelt**: Erkennt sie eine geänderte Bilddatei, wird deren URL aus
|
||||
der Hash-Erinnerung entfernt → die KI lädt das Bild neu und **bewertet seinen Inhalt neu** (neue
|
||||
Bytes → neuer Hash → Analyse). So greifen Integritäts- und Inhaltsprüfung ineinander.
|
||||
|
||||
Audio- und Video-Prüfung sind als abschaltbare Erweiterungspunkte vorbereitet
|
||||
(`ai_analysis.audio` / `ai_analysis.video`, default deaktiviert) und können aktiviert werden,
|
||||
|
|
|
|||
|
|
@ -689,6 +689,17 @@ def _run_assets_core(cfg: dict, bm: BaselineManager, snap: dict) -> tuple[dict,
|
|||
return asset_diff, score_asset_diff(asset_diff, cfg), bool(baseline_hashes)
|
||||
|
||||
|
||||
def _invalidate_ai_for_changed_assets(bm: BaselineManager, asset_diff: dict) -> None:
|
||||
"""Geänderte Bild-Assets (byte-getauscht) aus der KI-url_hashes-Erinnerung entfernen,
|
||||
damit die KI sie neu lädt UND inhaltlich neu bewertet. Nicht-Bild-URLs sind nicht in
|
||||
url_hashes → werden ignoriert."""
|
||||
changed_urls = [c["url"] for c in (asset_diff or {}).get("changed", []) if c.get("url")]
|
||||
n = bm.invalidate_ai_url_hashes(changed_urls)
|
||||
if n:
|
||||
logging.getLogger("scanner.assets").info(
|
||||
"KI-Cache für %d geänderte Bild(er) invalidiert → inhaltliche Neubewertung.", n)
|
||||
|
||||
|
||||
def cmd_check(args: argparse.Namespace, cfg: dict) -> int:
|
||||
logger = logging.getLogger("scanner.check")
|
||||
bm = BaselineManager(cfg["data_dir"])
|
||||
|
|
@ -762,6 +773,9 @@ def cmd_scan(args: argparse.Namespace, cfg: dict) -> int:
|
|||
levels.append(asset_assess["level"])
|
||||
score_total += asset_assess.get("score", 0)
|
||||
bm.mark_check_run("assets")
|
||||
# Geänderte Bilder → KI-Cache invalidieren, damit die KI sie inhaltlich neu
|
||||
# bewertet (läuft hier vor run_ai_analysis → Neubewertung noch im selben Scan).
|
||||
_invalidate_ai_for_changed_assets(bm, asset_diff)
|
||||
if pc.get("ext_links", True) and bm.is_check_due("ext_links", interval):
|
||||
print(" Wöchentliche Prüfung externer Links läuft mit ...")
|
||||
ext_links = _run_ext_links_core(cfg, snap) # informativ, hebt Level nicht an
|
||||
|
|
@ -1148,6 +1162,7 @@ def cmd_check_assets(args: argparse.Namespace, cfg: dict) -> int:
|
|||
print("Prüfe die Dateien Ihrer Website (Bilder/Skripte/Stylesheets) ...")
|
||||
asset_diff, assessment, has_baseline = _run_assets_core(cfg, bm, snap)
|
||||
bm.mark_check_run("assets")
|
||||
_invalidate_ai_for_changed_assets(bm, asset_diff)
|
||||
|
||||
report = {
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
|
|
|
|||
|
|
@ -292,6 +292,22 @@ class BaselineManager:
|
|||
json.dumps(state, indent=2, ensure_ascii=False), encoding="utf-8"
|
||||
)
|
||||
|
||||
def invalidate_ai_url_hashes(self, urls: list[str]) -> int:
|
||||
"""Entfernt URLs aus der url_hashes-Erinnerung. Folge: Die KI lädt diese Bilder
|
||||
beim nächsten Lauf erneut und bewertet sie inhaltlich neu (geänderte Bytes →
|
||||
neuer Hash → Cache-Miss → Analyse). Wird von der Datei-Prüfung (check-assets)
|
||||
für geänderte Bild-Assets aufgerufen. Returns Anzahl entfernter Einträge."""
|
||||
if not urls:
|
||||
return 0
|
||||
ledger = self.load_ai_ledger()
|
||||
uh = ledger.get("url_hashes", {})
|
||||
removed = [u for u in urls if u in uh]
|
||||
for u in removed:
|
||||
del uh[u]
|
||||
if removed:
|
||||
self.save_ai_ledger(ledger)
|
||||
return len(removed)
|
||||
|
||||
def dismiss_ai_entries(self, fingerprints: list[str] | None = None) -> int:
|
||||
"""
|
||||
Mark ledger entries as dismissed (false-positive acknowledgement).
|
||||
|
|
|
|||
|
|
@ -287,6 +287,49 @@ class TestImages:
|
|||
m2.assert_not_called()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# check-assets ↔ KI-Kopplung: geänderte Bilder werden neu bewertet
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestAssetAiCoupling:
|
||||
def _cfg_img(self):
|
||||
cfg = _cfg()
|
||||
cfg["ai_analysis"]["text"]["enabled"] = False
|
||||
cfg["ai_analysis"]["image"]["enabled"] = True
|
||||
return cfg
|
||||
|
||||
def test_invalidate_removes_only_given_urls(self, tmp_path):
|
||||
bm = BaselineManager(tmp_path)
|
||||
bm.save_ai_ledger({"entries": {}, "url_hashes": {"a": "h1", "b": "h2", "c": "h3"}})
|
||||
assert bm.invalidate_ai_url_hashes(["b", "x"]) == 1 # x ist nicht vorhanden
|
||||
assert set(bm.load_ai_ledger()["url_hashes"]) == {"a", "c"}
|
||||
|
||||
def test_changed_image_reanalyzed_after_invalidation(self, tmp_path, monkeypatch):
|
||||
"""Byte-Tausch eines bekannten Bildes → invalidieren → KI lädt+bewertet neu."""
|
||||
monkeypatch.setenv("OPENROUTER_API_KEY", "test")
|
||||
bm = BaselineManager(tmp_path)
|
||||
cfg = self._cfg_img()
|
||||
snap = _snap("kurz", img=[{"url": "https://x.de/logo.png", "class": "internal"}])
|
||||
|
||||
with patch("scanner.ai_analyzer.fetch_asset_hashes",
|
||||
return_value={"https://x.de/logo.png": {"sha256": "OLD", "size": 1, "error": None}}), \
|
||||
patch("scanner.ai_analyzer._openrouter_chat", return_value=_verdict()):
|
||||
run_ai_analysis(cfg, bm, snap, {})
|
||||
|
||||
# check-assets erkennt Byte-Änderung → Kopplung invalidiert die URL
|
||||
assert bm.invalidate_ai_url_hashes(["https://x.de/logo.png"]) == 1
|
||||
|
||||
new = _verdict("pornography", "high", 0.95)
|
||||
with patch("scanner.ai_analyzer.fetch_asset_hashes",
|
||||
return_value={"https://x.de/logo.png": {"sha256": "NEW", "size": 1, "error": None}}) as f2, \
|
||||
patch("scanner.ai_analyzer._openrouter_chat", return_value=new) as c2:
|
||||
res = run_ai_analysis(cfg, bm, snap, {})
|
||||
f2.assert_called() # neu geladen (URL war invalidiert)
|
||||
c2.assert_called() # neue Bytes (NEW) → Cache-Miss → neu analysiert
|
||||
assert res["api_calls"] == 1
|
||||
assert len(res["findings"]) == 1 # jetzt beanstandet
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scoring: cap at yellow
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue