fix(cookie+impressum): Drittland-FP, Impressum-Beleg, neuer Opt-Out-Finding
- Drittland: unbekannte Herkunft ('N/A') + Self-Hosting feuern nicht mehr —
First-Party-Session-Cookies (PHPSESSID/JSESSIONID) waren False Positives.
- Impressum _line_of: enges Fenster um den Treffer bei Texten ohne Umbrüche
(BMW = ein Block) → jede Pflichtangabe zeigt IHREN Beleg statt denselben Satz.
- Neuer Finding-Typ missing_opt_out: einwilligungspflichtiger Anbieter mit
Cookies ohne Opt-Out-/Widerspruchs-Link (Art. 7 Abs. 3 + Art. 21).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,7 @@ _CONTROL_MAP = {
|
||||
"excessive_lifetime": {"control_id": "AUTH-2051-A02", "regulation": "DSGVO", "article": "Art. 5 Abs. 1 lit. e"},
|
||||
"tracker_as_necessary": {"control_id": "DATA-2851-A05", "regulation": "TDDDG", "article": "§ 25 Abs. 1"},
|
||||
"missing_purpose": {"control_id": "AUTH-2053-A05", "regulation": "DSGVO", "article": "Art. 13"},
|
||||
"missing_opt_out": {"control_id": "DATA-2851-A05", "regulation": "DSGVO", "article": "Art. 7 Abs. 3 + Art. 21"},
|
||||
"third_country": {"control_id": "DATA-1624-A04", "regulation": "DSGVO", "article": "Art. 44 ff."},
|
||||
"eu_alternative": {"control_id": None, "regulation": "—", "article": "kommerzielle Empfehlung"},
|
||||
}
|
||||
@@ -72,6 +73,12 @@ _EEA = {
|
||||
"FI", "GR", "HU", "IT", "LV", "LT", "LU", "MT", "PL", "PT", "RO", "SK",
|
||||
"SI", "ES", "SE", "IS", "LI", "NO",
|
||||
}
|
||||
# Unbekannte/leere Herkunft ist KEIN Drittland (z.B. First-Party-Session-Cookies
|
||||
# PHPSESSID/JSESSIONID mit vendor_country 'N/A').
|
||||
_UNKNOWN_COUNTRY = {"", "N/A", "NA", "N.A.", "UNKNOWN", "UNBEKANNT", "?"}
|
||||
# Einwilligungspflichtige Kategorien (für Opt-Out-/Widerspruchs-Pflicht).
|
||||
_CONSENT_CATS = {"marketing", "statistics", "targeting", "social_media",
|
||||
"tracking", "werbung", "advertising"}
|
||||
_SEV_ORDER = {"HIGH": 0, "MEDIUM": 1, "LOW": 2}
|
||||
|
||||
|
||||
@@ -218,8 +225,13 @@ def analyze_cookies(vendors: list[dict], big_lib: dict | None = None) -> dict:
|
||||
),
|
||||
})
|
||||
|
||||
# 4) Drittland-Transfer (je Vendor einmal).
|
||||
if (country and country not in _EEA or schrems) and vname not in seen_third:
|
||||
# 4) Drittland-Transfer (je Vendor einmal). Nur bei BEKANNTEM
|
||||
# Nicht-EWR-Land — 'N/A'/unbekannt ist KEIN Drittland (First-Party-
|
||||
# Session-Cookies); Self-Hosting laut Library = kein Transfer.
|
||||
country_third = (country not in _UNKNOWN_COUNTRY
|
||||
and country not in _EEA
|
||||
and "SELF-HOST" not in country)
|
||||
if (country_third or schrems) and vname not in seen_third:
|
||||
seen_third.add(vname)
|
||||
findings.append({
|
||||
"vendor": vname, "cookie": name, "type": "third_country",
|
||||
@@ -271,6 +283,23 @@ def analyze_cookies(vendors: list[dict], big_lib: dict | None = None) -> dict:
|
||||
),
|
||||
})
|
||||
|
||||
# Vendor-Ebene: einwilligungspflichtiger Anbieter (Marketing/Tracking)
|
||||
# mit Cookies, aber ohne Opt-Out-/Widerspruchs-Link.
|
||||
if (vcat in _CONSENT_CATS and (v.get("cookies") or [])
|
||||
and not (v.get("opt_out_url") or "").strip()):
|
||||
findings.append({
|
||||
"vendor": vname, "cookie": "(Vendor-Ebene)",
|
||||
"type": "missing_opt_out", "severity": "LOW",
|
||||
"declared": vcat_label, "library_purpose": "",
|
||||
"remediation": (
|
||||
f"Für den einwilligungspflichtigen Anbieter '{vname}' "
|
||||
f"({vcat_label}) ist kein Opt-Out-/Widerspruchs-Link "
|
||||
f"hinterlegt. Eine einfache Widerrufs-/Widerspruchs-Möglichkeit "
|
||||
f"angeben (Art. 7 Abs. 3 + Art. 21 DSGVO, § 25 TDDDG) — so "
|
||||
f"einfach wie die Einwilligung."
|
||||
),
|
||||
})
|
||||
|
||||
# A: jeden Befund an Control + Rechtsgrundlage haengen + als echtes Finding
|
||||
# (zu beheben) oder Hinweis (advisory, gegen DSE abzugleichen) klassifizieren.
|
||||
for f in findings:
|
||||
|
||||
@@ -79,12 +79,18 @@ def _build_measure(label: str, norm: str) -> str:
|
||||
|
||||
|
||||
def _line_of(text: str, start_pos: int, end_pos: int) -> str:
|
||||
"""Die Zeile um einen Regex-Treffer — als 'gefundener Wert' für die
|
||||
Pflichtangaben-Tabelle. Gekappt + bereinigt."""
|
||||
"""Ein enger Ausschnitt um einen Regex-Treffer — der 'gefundene Wert' für die
|
||||
Pflichtangaben-Tabelle. Bevorzugt die Zeile; bei Texten ohne (genug)
|
||||
Zeilenumbrüche (z.B. BMW-Impressum als ein Block) ein Fenster um den Treffer,
|
||||
damit jede MC IHREN Beleg zeigt statt immer denselben Anfangssatz."""
|
||||
start = text.rfind("\n", 0, start_pos) + 1
|
||||
end = text.find("\n", end_pos)
|
||||
if end == -1:
|
||||
end = len(text)
|
||||
# Zeile zu lang (kein/seltener Umbruch) → enges Fenster zentriert am Treffer.
|
||||
if end - start > 160:
|
||||
start = max(start, start_pos - 70)
|
||||
end = min(end, end_pos + 70)
|
||||
return " ".join(text[start:end].split())[:160]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user