From 4171cf0efd5927d175f06317edcee4372fa63c9a Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 21 May 2026 16:45:12 +0200 Subject: [PATCH] feat(audit): P36 Social-Media-Einbindungs-Check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit check_social_embedding: erkennt direkte FB/Insta/Twitter/YouTube- Embeds (connect.facebook.net, platform.twitter.com etc) vs Heise-Shariff vs 2-Klick-Loesungen (Embetty). Direkte Embeds ohne Schutz = HIGH (EuGH C-40/17 Fashion-ID — der Site-Betreiber wird zum gemeinsam Verantwortlichen und braucht Einwilligung VOR dem Drittanbieter-Call). Shariff oder 2-Klick erkannt = INFO (positives Signal). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../compliance/services/doc_text_signals.py | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/backend-compliance/compliance/services/doc_text_signals.py b/backend-compliance/compliance/services/doc_text_signals.py index 49e8c619..8a479261 100644 --- a/backend-compliance/compliance/services/doc_text_signals.py +++ b/backend-compliance/compliance/services/doc_text_signals.py @@ -59,6 +59,26 @@ _JC_PATTERNS = ( "gemeinsame verarbeitung", ) +# P36 — Social-Media-Einbindung: +# "direct" = direkte FB/Insta/Twitter-Embeds laden bei Page-Load +# (HIGH-Risiko, Cookies vor Consent). +# "shariff" = Heise-Shariff-Buttons (clientseitig, kein 3rd-party-Call). +# "two_click" = zweistufige Loesung (Klick auf Platzhalter laed Tracker). +_SOCIAL_DIRECT_PATTERNS = ( + "connect.facebook.net", "platform.twitter.com", + "platform.instagram.com", "platform.linkedin.com", + "youtube.com/embed", "syndication.twitter.com", + "//www.facebook.com/", "fb-pixel", "facebook-pixel", +) +_SOCIAL_SHARIFF_PATTERNS = ( + "shariff", "ct_shariff", "data-shariff", +) +_SOCIAL_TWOCLICK_PATTERNS = ( + "2-klick", "2klick", "zwei klick", "two-click", + "klick-zu-laden", "klick um zu laden", "platzhalter laed", + "embetty", +) + def check_save_only_reject(banner_result: dict) -> dict | None: """P35 — Banner hat keinen klaren Reject, nur "Speichern".""" @@ -150,10 +170,67 @@ def check_jc_clause_in_dse(doc_texts: dict[str, str]) -> dict | None: } +def check_social_embedding( + doc_texts: dict[str, str], + homepage_html: str | None = None, +) -> dict | None: + """P36 — direkte Social-Embeds vs Shariff vs 2-Klick.""" + sources: list[str] = [] + for key in ("dse", "cookie", "impressum"): + v = (doc_texts or {}).get(key) or "" + if v: + sources.append(v[:50000]) + if homepage_html: + sources.append(homepage_html[:50000]) + if not sources: + return None + blob = " ".join(sources).lower() + direct_hits = [p for p in _SOCIAL_DIRECT_PATTERNS if p in blob] + has_shariff = any(p in blob for p in _SOCIAL_SHARIFF_PATTERNS) + has_twoclick = any(p in blob for p in _SOCIAL_TWOCLICK_PATTERNS) + + if not direct_hits and not has_shariff and not has_twoclick: + return None + if direct_hits and not (has_shariff or has_twoclick): + return { + "severity": "HIGH", + "code": "social_direct_embed", + "label": "Direkte Social-Media-Embeds ohne 2-Klick-Schutz " + "oder Shariff erkannt", + "detail": ( + f'Gefundene Drittanbieter-Skripte: ' + f'{", ".join(sorted(set(direct_hits))[:4])}. ' + "Diese laden i.d.R. Cookies/Pixel ohne Einwilligung. " + "Empfehlung: Heise-Shariff (clientseitig) oder " + "2-Klick-Loesung (Embetty, eigener Platzhalter)." + ), + "legal_basis": "EuGH C-40/17 (Fashion-ID) — Einbinden eines " + "Facebook-Like-Buttons macht den Site-Betreiber " + "zum gemeinsam Verantwortlichen + benoetigt " + "Einwilligung VOR dem Drittanbieter-Call.", + } + if has_shariff or has_twoclick: + return { + "severity": "INFO", + "code": "social_protected_embed", + "label": ( + "Datenschutzfreundliche Social-Media-Einbindung erkannt " + f"({'Shariff' if has_shariff else '2-Klick-Loesung'})" + ), + "detail": ( + "Drittanbieter-Skripte werden erst nach aktivem Klick " + "geladen — kein Tracking ohne Einwilligung." + ), + "legal_basis": "EuGH C-40/17 + EDPB Guidelines 8/2020.", + } + return None + + def run_all( banner_result: dict | None, doc_texts: dict[str, str] | None, cookie_doc_missing: bool = False, + homepage_html: str | None = None, ) -> list[dict]: findings: list[dict] = [] try: @@ -174,6 +251,12 @@ def run_all( findings.append(f) except Exception as e: logger.warning("P78 jc_clause failed: %s", e) + try: + f = check_social_embedding(doc_texts or {}, homepage_html) + if f: + findings.append(f) + except Exception as e: + logger.warning("P36 social_embedding failed: %s", e) return findings