integrity_scanner_fuer_stat.../scanner/report.py

282 lines
12 KiB
Python
Raw Permalink Normal View History

"""
Gemeinsamer Report-Renderer.
EINE Quelle der Wahrheit für den Dateibericht (report.md) UND den E-Mail-Body.
So können Report und Alarm-Mail nicht mehr auseinanderlaufen. Alle Kommandos
enthalten das Target (direkt ausführbar); der angezeigte Score ist der bereits
aggregierte Gesamt-Score (Inhalt + KI + Zusatzprüfungen).
"""
from .plain import ampel, klartext_befunde, scanner_cmd, was_tun
def render_markdown(r: dict) -> str:
a = r.get("assessment", {})
d = r.get("diff", {})
target = r.get("target", "")
lvl = a.get("level", "green")
level = lvl.upper()
score = a.get("score", 0)
level_badge = {"GREEN": "[OK]", "YELLOW": "[WARNUNG]", "RED": "[ALARM]"}.get(level, level)
symbol, headline = ampel(lvl)
ai = r.get("ai_result")
config_path = r.get("config_path")
scan_cmd = (f"python -m scanner --config {config_path} scan"
if config_path else f"python -m scanner {target} scan")
# ---- Klartext-Teil (für den Betreiber) ----
lines = [
f"# Prüfbericht für Ihre Website — {target or '?'}",
"",
f"**Erstellt:** {r.get('generated_at', '?')} ",
f"**Kommando:** `{scan_cmd}`",
"",
f"## {symbol} {headline}",
"",
"### Was bedeutet das?",
"",
]
for satz in klartext_befunde(d, r.get("cloak_diff"), r.get("asset_diff"),
r.get("ext_links"), ai):
lines.append(f"- {satz}")
lines += ["", "### Was sollten Sie tun?", ""]
for schritt in was_tun(lvl, target):
lines.append(f"- {schritt}")
# ---- Technischer Teil (für den Dienstleister) ----
ai_funde = len(ai.get("findings", [])) if ai else 0
ai_ungeprueft = len(ai.get("unchecked", [])) if ai else 0
lines += [
"",
"---",
"",
"# Technische Details (für Ihren Dienstleister)",
"",
f"**Level:** {level_badge} ",
f"**Score:** {score}",
"",
"## Zusammenfassung",
"",
"| Kennzahl | Wert |",
"|---|---|",
f"| Geprüfte Seiten | {d.get('total_pages_checked', '?')} |",
f"| Geänderte Seiten | {d.get('changed_pages', 0)} |",
f"| Neue interne URLs | {len(d.get('new_internal_urls', []))} |",
f"| Fehlende URLs | {len(d.get('missing_internal_urls', []))} |",
f"| Neue externe Domains | {len(d.get('new_external_domains', []))} |",
f"| Kaputte Links (neu) | {len(d.get('new_broken_urls', []))} |",
f"| Kaputte Links (bekannt) | {len(d.get('known_broken_urls', []))} |",
f"| Unerwartetes JSON-LD | {len(d.get('unexpected_jsonld', []))} |",
f"| Verdächtige Dateinamen | {len(d.get('suspicious_filenames', []))} |",
f"| KI-Funde (Text/Bild) | {ai_funde} |",
f"| KI nicht geprüft | {ai_ungeprueft} |",
f"| Whitelist-Verstösse | {len(r.get('whitelist_violations', []))} |",
f"| Crawl-Fehler | {len(r.get('crawl_errors', []))} |",
"",
"## Risikobewertung",
"",
]
for reason in a.get("reasons", []):
lines.append(f"- {reason}")
if not a.get("reasons"):
lines.append("- Keine Auffälligkeiten.")
if d.get("new_external_domains"):
lines += ["", "## Neue externe Domains", ""]
for dom in d["new_external_domains"]:
lines.append(f"- `{dom}`")
if d.get("new_internal_urls"):
lines += ["", "## Neue interne URLs", ""]
for url in d["new_internal_urls"]:
lines.append(f"- {url}")
if d.get("missing_internal_urls"):
lines += ["", "## Fehlende URLs (waren in Baseline)", ""]
for url in d["missing_internal_urls"]:
lines.append(f"- {url}")
if d.get("page_diffs"):
lines += ["", "## Geänderte Seiten", ""]
for pd in d["page_diffs"]:
lines += [f"### {pd['url']}", ""]
if pd.get("added_chars"):
lines.append(f"- Neuer Text: +{pd['added_chars']} Zeichen")
if pd.get("meta_diff"):
for field, change in pd["meta_diff"].items():
if isinstance(change, dict) and "old" in change:
lines.append(f"- {field}: `{change['old']}` → `{change['new']}`")
else:
lines.append(f"- {field}: {change}")
if pd.get("canonical_changed"):
lines.append(
f"- Canonical geändert: `{pd.get('old_canonical')}` → `{pd.get('new_canonical')}`"
)
if pd.get("new_hidden_content"):
for h in pd["new_hidden_content"]:
lines.append(f"- Hidden Content ({h.get('type')}): `{', '.join(h.get('indicators', []))}`")
if pd.get("new_meta_refresh"):
for mr in pd["new_meta_refresh"]:
lines.append(f"- Meta-Refresh → `{mr}`")
if pd.get("new_comment_links"):
for cl in pd["new_comment_links"]:
lines.append(f"- Kommentar-Link: `{cl}`")
if pd.get("new_inline_scripts"):
for s in pd["new_inline_scripts"]:
lines.append(f"- Inline-Script: `{', '.join(s.get('patterns', []))}`")
if pd.get("link_diff"):
for ltype, ld in pd["link_diff"].items():
for added in ld.get("added", []):
lines.append(f"- {ltype} hinzugefügt: `{added}`")
for removed in ld.get("removed", []):
lines.append(f"- {ltype} entfernt: `{removed}`")
if pd.get("text_diff"):
lines += ["", "```diff"]
lines += pd["text_diff"][:40]
if len(pd["text_diff"]) > 40:
lines.append(f"... ({len(pd['text_diff']) - 40} weitere Zeilen)")
lines.append("```")
lines.append("")
if d.get("new_pages_analysis"):
lines += ["", "## Neue Seiten (Erstanalyse)", ""]
for npa in d["new_pages_analysis"]:
problems = (
npa.get("hidden_content")
or npa.get("inline_scripts")
or npa.get("meta_refresh")
or npa.get("unexpected_jsonld")
or npa.get("comment_links")
)
flag = " [VERDAECHTIG]" if problems else ""
lines.append(f"- {npa['url']}{flag}")
new_broken = d.get("new_broken_urls", [])
known_broken = d.get("known_broken_urls", [])
if new_broken or known_broken:
lines += ["", "## Kaputte Links", ""]
for entry in new_broken:
lines.append(f"- [NEU] HTTP {entry['status']} `{entry['url']}`")
for page in entry.get("pages", [])[:3]:
lines.append(f" - gefunden auf: {page}")
for entry in known_broken:
lines.append(f"- [bekannt, noch nicht behoben] HTTP {entry['status']} `{entry['url']}`")
for page in entry.get("pages", [])[:3]:
lines.append(f" - gefunden auf: {page}")
if d.get("unexpected_jsonld"):
lines += ["", "## Unerwartetes JSON-LD", ""]
for entry in d["unexpected_jsonld"]:
lines.append(f"- `{entry['url']}`: @type={entry['type']}")
if d.get("suspicious_filenames"):
lines += ["", "## Verdächtige Dateinamen", ""]
for entry in d["suspicious_filenames"]:
lines.append(f"- `{entry['page']}` → `{entry['url']}` ({entry['link_type']})")
if r.get("whitelist_violations"):
lines += ["", "## Whitelist-Verstösse", ""]
for v in r["whitelist_violations"][:50]:
lines.append(f"- [{v.get('type')}] {v.get('url', '')} (Domain: {v.get('domain', '')})")
if r.get("missing_security_headers"):
lines += ["", "## Fehlende Security-Header", ""]
for url, hdrs in r["missing_security_headers"].items():
for h in hdrs:
lines.append(f"- `{url}`: {h}")
if r.get("crawl_errors"):
lines += ["", "## Crawl-Fehler", ""]
for e in r["crawl_errors"][:20]:
lines.append(f"- `{e.get('url', '?')}`: {e.get('error', '?')}")
# ---- Ergebnisse der wöchentlichen Zusatzprüfungen (falls gelaufen) ----
cd = r.get("cloak_diff")
if cd is not None:
lines += ["", "## Tarnungs-Prüfung (Cloaking)", ""]
if cd.get("findings"):
for f in cd["findings"]:
lines.append(f"- `{f['url']}`")
if f.get("extra_links"):
for link in f["extra_links"]:
lines.append(f" - Nur für Google sichtbarer Link: `{link}`")
if f.get("extra_text_chars", 0) > 50:
lines.append(f" - Google sieht +{f['extra_text_chars']} Zeichen mehr Text")
if f.get("vary_ua"):
lines.append(" - Server liefert je nach Besucher anderen Inhalt (Vary: User-Agent)")
else:
lines.append("- Keine Auffälligkeiten.")
ad = r.get("asset_diff")
if ad is not None:
lines += ["", "## Datei-Prüfung (Bilder/Skripte/Stylesheets)", ""]
if ad.get("changed"):
for entry in ad["changed"]:
size_info = f" ({entry['size_diff']:+d} Bytes)" if entry.get("size_diff") else ""
lines.append(f"- Geändert [{entry['type']}]: `{entry['url']}`{size_info}")
else:
lines.append("- Keine Datei-Änderungen.")
if ad.get("fetch_errors"):
for entry in ad["fetch_errors"]:
lines.append(f"- Nicht prüfbar: `{entry['url']}` ({entry['error']})")
el = r.get("ext_links")
if el is not None:
lines += ["", "## Externe Links (Erreichbarkeit)", ""]
lines.append(f"- Geprüft: {el.get('total', 0)}, kaputt: {el.get('broken', 0)}, "
f"nicht erreichbar: {el.get('errors', 0)}")
for entry in el.get("broken_links", []):
lines.append(f"- HTTP {entry['status']}: `{entry['url']}`")
for page in entry.get("pages", [])[:3]:
lines.append(f" - gefunden auf: {page}")
for entry in el.get("error_links", []):
lines.append(f"- Nicht erreichbar: `{entry['url']}` ({entry.get('error', '?')})")
for page in entry.get("pages", [])[:3]:
lines.append(f" - gefunden auf: {page}")
if ai is not None:
lines += ["", "## KI-Inhaltsanalyse", ""]
if ai.get("skipped"):
lines.append(f"- Übersprungen: {ai['skipped']}")
else:
lines.append(
f"- {ai.get('checked', 0)} Inhalt(e) geprüft, "
f"{ai.get('cache_hits', 0)} aus Cache, {ai.get('api_calls', 0)} API-Abfrage(n)."
)
if ai.get("unchecked"):
lines.append(f"- ⚠ {len(ai['unchecked'])} Inhalt(e) **NICHT geprüft** "
"(KI-Dienst nicht erreichbar) — „nicht geprüft“ ≠ „sauber“:")
for u in ai["unchecked"][:20]:
ziel = u.get("asset_url") or u.get("url", "?")
lines.append(f" - [{u.get('kind', '?')}] `{ziel}`")
for f in ai.get("findings", []):
asset = f.get("asset_url")
ziel = f"`{asset}` (auf {f['url']})" if asset else f"`{f['url']}`"
lines.append(
f"- [{f['kind']}] {ziel}: **{f['category']}** "
f"(Schwere {f['severity']}, Konfidenz {f['confidence']:.0%})"
)
if f.get("explanation"):
lines.append(f" - Begründung: {f['explanation']}")
if f.get("fingerprint"):
lines.append(" - Quittieren bei Fehlalarm: "
f"`{scanner_cmd(target, 'ai-dismiss --hash ' + f['fingerprint'])}`")
lines += ["", "---", "", "## Nächste Schritte", ""]
lev = a.get("level", "green")
if lev == "red":
lines += [
"1. **Sofort**: Website-Backend überprüfen, Dateien mit Backup vergleichen.",
"2. Dienstleister kontaktieren.",
f"3. Nach Bereinigung: `{scanner_cmd(target, 'approve --rebuild')}`",
]
elif lev == "yellow":
lines += [
"1. Alle markierten URLs manuell prüfen.",
f"2. Legitime Änderungen freigeben: `{scanner_cmd(target, 'approve --url <URL>')}`",
f"3. Alle freigeben (wenn geprüft): `{scanner_cmd(target, 'approve --all')}`",
]
else:
lines += ["Keine Aktion erforderlich."]
return "\n".join(lines) + "\n"