feat(cookie): ② Documentation Drift — Richtlinie vs. Browser-Realität
Cookie-Check-Endpoint liefert jetzt out["drift"] (audit_cookie_compliance): deklariert (Cookie-Richtlinie-Text) vs. tatsaechlich geladen (Browser). Frontend zeigt den Reality-Check-Strip oben im Panel: X dokumentiert · Y geladen · Z undokumentiert. Pinnt den Vertrag mit test_cookie_drift.py (undokumentiert-geladen + beide Drift-Richtungen) + Vitest Drift-Strip. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,12 @@ interface CheckData {
|
|||||||
real_cookies?: number
|
real_cookies?: number
|
||||||
other_storage?: number
|
other_storage?: number
|
||||||
}
|
}
|
||||||
|
drift?: {
|
||||||
|
declared_count?: number
|
||||||
|
browser_count?: number
|
||||||
|
high_findings?: number
|
||||||
|
low_findings?: number
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const SEV_COLOR: Record<string, string> = {
|
const SEV_COLOR: Record<string, string> = {
|
||||||
@@ -55,8 +61,24 @@ export function CookieFindingList({ data }: { data: CheckData }) {
|
|||||||
const findings = data.findings || []
|
const findings = data.findings || []
|
||||||
const s = data.summary || {}
|
const s = data.summary || {}
|
||||||
const inv = data.storage_inventory
|
const inv = data.storage_inventory
|
||||||
|
const drift = data.drift
|
||||||
|
const driftShown =
|
||||||
|
!!drift && ((drift.declared_count ?? 0) + (drift.browser_count ?? 0)) > 0
|
||||||
return (
|
return (
|
||||||
<div className="border rounded-lg overflow-hidden">
|
<div className="border rounded-lg overflow-hidden">
|
||||||
|
{driftShown && (
|
||||||
|
<div className="px-4 py-2.5 bg-amber-50 border-b text-xs text-amber-900">
|
||||||
|
<span className="font-semibold">Richtlinie ↔ Realität:</span>{' '}
|
||||||
|
<strong>{drift!.declared_count ?? 0}</strong> in der Cookie-Richtlinie
|
||||||
|
dokumentiert · <strong>{drift!.browser_count ?? 0}</strong> im Browser geladen
|
||||||
|
{(drift!.high_findings ?? 0) > 0 && (
|
||||||
|
<> · <strong className="text-red-700">{drift!.high_findings} undokumentiert geladen</strong></>
|
||||||
|
)}
|
||||||
|
{(drift!.low_findings ?? 0) > 0 && (
|
||||||
|
<> · {drift!.low_findings} dokumentiert, aber nicht geladen</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{inv && (inv.total ?? 0) > 0 && (
|
{inv && (inv.total ?? 0) > 0 && (
|
||||||
<div className="px-4 py-2.5 bg-blue-50 border-b text-xs text-blue-900">
|
<div className="px-4 py-2.5 bg-blue-50 border-b text-xs text-blue-900">
|
||||||
<span className="font-semibold">Storage-Inventar:</span>{' '}
|
<span className="font-semibold">Storage-Inventar:</span>{' '}
|
||||||
|
|||||||
@@ -27,6 +27,16 @@ describe('CookieFindingList', () => {
|
|||||||
expect(screen.getByText(/Keine Abweichungen/)).toBeInTheDocument()
|
expect(screen.getByText(/Keine Abweichungen/)).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('zeigt den Drift-Strip (Richtlinie vs. Browser-Realität)', () => {
|
||||||
|
render(<CookieFindingList data={{
|
||||||
|
summary: { checked: 31, in_library: 8, findings: 0 },
|
||||||
|
drift: { declared_count: 0, browser_count: 31, high_findings: 31, low_findings: 0 },
|
||||||
|
findings: [],
|
||||||
|
}} />)
|
||||||
|
expect(screen.getByText(/Richtlinie ↔ Realität/)).toBeInTheDocument()
|
||||||
|
expect(screen.getByText(/31 undokumentiert geladen/)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
it('zeigt das Storage-Inventar (echte Cookies vs. andere)', () => {
|
it('zeigt das Storage-Inventar (echte Cookies vs. andere)', () => {
|
||||||
render(<CookieFindingList data={{
|
render(<CookieFindingList data={{
|
||||||
summary: { checked: 100, in_library: 30, findings: 0 },
|
summary: { checked: 100, in_library: 30, findings: 0 },
|
||||||
|
|||||||
@@ -235,6 +235,9 @@ async def snapshot_cookie_check(snapshot_id: str):
|
|||||||
from compliance.services.cookie_storage_inventory import (
|
from compliance.services.cookie_storage_inventory import (
|
||||||
build_storage_inventory, storage_transparency_finding,
|
build_storage_inventory, storage_transparency_finding,
|
||||||
)
|
)
|
||||||
|
from compliance.services.cookie_compliance_audit import (
|
||||||
|
audit_cookie_compliance,
|
||||||
|
)
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
snap = load_snapshot(db, snapshot_id)
|
snap = load_snapshot(db, snapshot_id)
|
||||||
@@ -251,6 +254,15 @@ async def snapshot_cookie_check(snapshot_id: str):
|
|||||||
out["findings"].insert(0, tf)
|
out["findings"].insert(0, tf)
|
||||||
out["summary"]["findings"] = len(out["findings"])
|
out["summary"]["findings"] = len(out["findings"])
|
||||||
out["storage_inventory"] = inv
|
out["storage_inventory"] = inv
|
||||||
|
# ② Documentation Drift: Cookie-Richtlinie (Text) vs. Browser-Realität.
|
||||||
|
docs = snap.get("doc_entries") or []
|
||||||
|
cookie_text = next(
|
||||||
|
(e.get("text") or e.get("content") or "" for e in docs
|
||||||
|
if e.get("doc_type") in ("cookie", "cookie_richtlinie", "cookies")),
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
out["drift"] = audit_cookie_compliance(
|
||||||
|
db, cookie_text, snap.get("banner_result"))
|
||||||
return out
|
return out
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
"""② Documentation Drift — Cookie-Richtlinie (deklariert) vs. Browser-Realität.
|
||||||
|
|
||||||
|
Pinnt den Vertrag, den der Endpoint (snapshot_cookie_check → out["drift"]) und
|
||||||
|
das Frontend (CookieFindingList Drift-Strip) konsumieren: declared_count /
|
||||||
|
browser_count / high_findings / low_findings + die undokumentiert-geladen-Liste.
|
||||||
|
db=None → Library-Lookup übersprungen, Drift bleibt rein mengenbasiert.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from compliance.services.cookie_compliance_audit import audit_cookie_compliance
|
||||||
|
|
||||||
|
|
||||||
|
def _banner(*cookie_names: str) -> dict:
|
||||||
|
return {"phases": {"after_accept": {
|
||||||
|
"cookies": [{"name": n} for n in cookie_names]}}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_undeclared_browser_cookies_are_high_drift():
|
||||||
|
# BMW-Fall: Richtlinie nennt nichts, Browser lädt Tracker → undokumentiert.
|
||||||
|
audit = audit_cookie_compliance(None, None, _banner("_ga", "_fbp"))
|
||||||
|
assert audit["declared_count"] == 0
|
||||||
|
assert audit["browser_count"] == 2
|
||||||
|
assert audit["high_findings"] == 2
|
||||||
|
assert set(audit["undeclared_in_browser"]) == {"_ga", "_fbp"}
|
||||||
|
assert audit["low_findings"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_empty_inputs_yield_zero_drift():
|
||||||
|
audit = audit_cookie_compliance(None, None, None)
|
||||||
|
for k in ("declared_count", "browser_count", "high_findings", "low_findings"):
|
||||||
|
assert audit[k] == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_drift_both_directions():
|
||||||
|
# Richtlinie deklariert 3 Cookies (Tab-Tabelle), Browser lädt 2 davon + 1 neuen.
|
||||||
|
doc = (
|
||||||
|
"Name\tKategorie\tZweck\tSpeicherdauer\n"
|
||||||
|
"_ga\tAnalytics\tBesucher unterscheiden\t2 Jahre\n"
|
||||||
|
"_gid\tAnalytics\tBesucher unterscheiden\t1 Tag\n"
|
||||||
|
"consent\tNotwendig\tEinwilligung speichern\t1 Jahr\n"
|
||||||
|
)
|
||||||
|
audit = audit_cookie_compliance(None, doc, _banner("_ga", "_gid", "_fbp"))
|
||||||
|
# _fbp ist geladen aber nicht deklariert → HIGH-Drift.
|
||||||
|
assert "_fbp" in audit["undeclared_in_browser"]
|
||||||
|
assert audit["high_findings"] >= 1
|
||||||
|
# consent ist deklariert aber nicht geladen → LOW-Drift.
|
||||||
|
assert "consent" in audit["declared_not_loaded"]
|
||||||
|
assert audit["low_findings"] >= 1
|
||||||
Reference in New Issue
Block a user