""" 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 ')}`", f"3. Alle freigeben (wenn geprüft): `{scanner_cmd(target, 'approve --all')}`", ] else: lines += ["Keine Aktion erforderlich."] return "\n".join(lines) + "\n"