fix: Report und Alarm-Mail konsistent (gemeinsamer Renderer)
Behebt vier vom Nutzer gemeldete Inkonsistenzen zwischen Report und E-Mail: 1. E-Mail ⊊ Report: Es gab zwei divergierende Renderer (alerter._format_body und __main__._render_markdown). Jetzt EIN gemeinsamer Renderer (scanner/report.py:render_markdown), den beide nutzen → identischer Inhalt, keine Divergenz mehr möglich. 2. KI im Report unsichtbar: Der Report rief klartext_befunde OHNE ai_result auf (KI-Satz fehlte im "Was bedeutet das?") und die Zusammenfassungs-Tabelle hatte keine KI-Zeile. Jetzt: KI-Satz im Klartext + Zeilen "KI-Funde" und "KI nicht geprüft" in der Tabelle. 3. Fehlende URL in Kommandos: approve/report/ai-dismiss standen ohne Target da (nicht ausführbar). Neuer Helfer plain.scanner_cmd(target, rest) injiziert das Target überall → "python -m scanner https://site approve --all". 4. "Level: YELLOW" aber "Score: 0": Der angezeigte Score war nur der Inhalts- Diff-Score; KI-/Cloak-/Asset-Scores flossen ins Level, aber nicht in den Score. cmd_scan/cmd_check aggregieren jetzt alle Teil-Scores → Score und Level passen zusammen. Tests: 254 grün (+4: Body==Report, Kommandos-mit-Target, Score-Anzeige, Level-Badge). alerter._format_body delegiert nur noch an render_markdown. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
125d80e7f9
commit
3ab313e733
5 changed files with 327 additions and 398 deletions
|
|
@ -6,12 +6,12 @@ Alerts are only sent when the assessment level meets or exceeds min_level.
|
|||
import logging
|
||||
import os
|
||||
import smtplib
|
||||
from datetime import datetime, timezone
|
||||
from email.message import EmailMessage
|
||||
|
||||
import requests
|
||||
|
||||
from .plain import ampel, klartext_befunde, was_tun
|
||||
from .plain import ampel
|
||||
from .report import render_markdown
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -56,120 +56,9 @@ def _make_subject(report: dict) -> str:
|
|||
|
||||
|
||||
def _format_body(report: dict) -> str:
|
||||
assessment = report.get("assessment", {})
|
||||
level = assessment.get("level", "green")
|
||||
diff = report.get("diff", {})
|
||||
symbol, headline = ampel(level)
|
||||
|
||||
# ---- Klartext-Teil (für den Betreiber) ----
|
||||
lines = [
|
||||
f"Prüfbericht für Ihre Website: {report.get('target', '?')}",
|
||||
f"Zeitpunkt: {datetime.now(timezone.utc).strftime('%d.%m.%Y %H:%M Uhr')} (UTC)",
|
||||
"",
|
||||
f"{symbol} {headline}",
|
||||
"",
|
||||
"Was bedeutet das?",
|
||||
]
|
||||
for satz in klartext_befunde(
|
||||
diff, report.get("cloak_diff"), report.get("asset_diff"),
|
||||
report.get("ext_links"), report.get("ai_result"),
|
||||
):
|
||||
lines.append(f" • {satz}")
|
||||
lines += ["", "Was sollten Sie tun?"]
|
||||
for schritt in was_tun(level):
|
||||
lines.append(f" → {schritt}")
|
||||
|
||||
# ---- Technischer Teil (für den Dienstleister) ----
|
||||
lines += [
|
||||
"",
|
||||
"=" * 60,
|
||||
"Technische Details (für Ihren Dienstleister)",
|
||||
"=" * 60,
|
||||
f"Level: {level.upper()} Score: {assessment.get('score', '?')}",
|
||||
"",
|
||||
"--- Gruende ---",
|
||||
]
|
||||
for reason in assessment.get("reasons", []):
|
||||
lines.append(f" * {reason}")
|
||||
|
||||
_section(lines, "Neue externe Domains", diff.get("new_external_domains", []), "+")
|
||||
_section(lines, "Neue interne URLs", diff.get("new_internal_urls", []), "+")
|
||||
_section(lines, "Fehlende URLs", diff.get("missing_internal_urls", []), "-")
|
||||
|
||||
for pd in diff.get("page_diffs", [])[:10]:
|
||||
lines += ["", f"--- Aenderung: {pd['url']} ---"]
|
||||
if pd.get("text_diff"):
|
||||
lines.append(f" Text-Diff: {pd['added_chars']} Zeichen zugefügt")
|
||||
lines += [f" {l}" for l in pd["text_diff"][:30]]
|
||||
if pd.get("new_hidden_content"):
|
||||
lines.append(f" Hidden Content: {len(pd['new_hidden_content'])} neue(r) Fund(e)")
|
||||
if pd.get("new_meta_refresh"):
|
||||
lines.append(f" Meta-Refresh: {pd['new_meta_refresh']}")
|
||||
if pd.get("canonical_changed"):
|
||||
lines.append(f" Canonical geaendert: {pd.get('old_canonical')} -> {pd.get('new_canonical')}")
|
||||
if pd.get("new_comment_links"):
|
||||
lines.append(f" Kommentar-Links: {pd['new_comment_links']}")
|
||||
|
||||
# ---- KI-Inhaltsanalyse: jede beanstandete Datei mit URL + Begründung ----
|
||||
ai = report.get("ai_result") or {}
|
||||
if ai.get("findings"):
|
||||
lines += ["", "--- KI-Inhaltsanalyse: beanstandete Inhalte ---"]
|
||||
for f in ai["findings"]:
|
||||
asset = f.get("asset_url")
|
||||
ziel = f"{asset} (auf Seite {f['url']})" if asset else f["url"]
|
||||
lines.append(f" [{f['kind']}] {ziel}")
|
||||
lines.append(f" Befund: {f['category']} "
|
||||
f"(Schwere {f['severity']}, Konfidenz {f.get('confidence', 0):.0%})")
|
||||
if f.get("explanation"):
|
||||
lines.append(f" Begründung: {f['explanation']}")
|
||||
if f.get("fingerprint"):
|
||||
lines.append(f" Quittieren: python -m scanner ai-dismiss --hash {f['fingerprint']}")
|
||||
if ai.get("unchecked"):
|
||||
lines += ["", "--- KI-Inhaltsanalyse: NICHT geprüfte Inhalte ---",
|
||||
" Achtung: Der Prüfdienst war nicht erreichbar. „Nicht geprüft“ ≠ „sauber“ —"
|
||||
" bitte beim nächsten Lauf erneut prüfen."]
|
||||
for u in ai["unchecked"]:
|
||||
ziel = u.get("asset_url") or u.get("url", "?")
|
||||
lines.append(f" [{u.get('kind', '?')}] {ziel}")
|
||||
|
||||
lines += [
|
||||
"",
|
||||
"--- Empfehlung ---",
|
||||
]
|
||||
level_lower = assessment.get("level", "green")
|
||||
if level_lower == "red":
|
||||
lines += [
|
||||
" SOFORT HANDELN: Website moeglicherweise kompromittiert.",
|
||||
" Zugriff auf Hosting-Backend prüfen, Dateien vergleichen.",
|
||||
]
|
||||
elif level_lower == "yellow":
|
||||
lines += [
|
||||
" PRÜFEN: Unerwartete Aenderungen gefunden.",
|
||||
" Alle markierten URLs manuell kontrollieren.",
|
||||
" Falls legitim: python -m scanner approve --all",
|
||||
]
|
||||
else:
|
||||
lines.append(" OK — nur zur Information.")
|
||||
|
||||
lines += [
|
||||
"",
|
||||
"--- Workflow ---",
|
||||
" Details: python -m scanner report",
|
||||
" Freigabe: python -m scanner approve --url <URL>",
|
||||
" Rebuild: python -m scanner approve --rebuild",
|
||||
"=" * 60,
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _section(lines: list, title: str, items: list, prefix: str) -> None:
|
||||
if not items:
|
||||
return
|
||||
lines += ["", f"--- {title} ---"]
|
||||
for item in items[:50]:
|
||||
lines.append(f" {prefix} {item}")
|
||||
if len(items) > 50:
|
||||
lines.append(f" ... und {len(items) - 50} weitere")
|
||||
"""E-Mail-Body = derselbe Renderer wie der Dateibericht (eine Quelle der
|
||||
Wahrheit). So enthalten Report und Mail garantiert denselben Inhalt."""
|
||||
return render_markdown(report)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue