ConsentOS — a privacy-first cookie consent management platform. Self-hosted, source-available alternative to OneTrust, Cookiebot, and CookieYes. Full standards coverage (IAB TCF v2.2, GPP v1, Google Consent Mode v2, GPC, Shopify Customer Privacy API), multi-tenant architecture with role-based access, configuration cascade (system → org → group → site → region), dark-pattern detection in the scanner, and a tamper-evident consent record audit trail. This is the initial public release. Prior development history is retained internally. See README.md for the feature list, architecture overview, and quick-start instructions. Licensed under the Elastic Licence 2.0 — self-host freely; do not resell as a managed service.
108 lines
3.1 KiB
Python
108 lines
3.1 KiB
Python
"""Cookie classification based on known patterns.
|
|
|
|
Matches discovered cookies against a database of known cookie patterns
|
|
to auto-categorise them (analytics, marketing, functional, etc.).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from dataclasses import dataclass
|
|
|
|
|
|
@dataclass
|
|
class KnownPattern:
|
|
"""A known cookie pattern for classification."""
|
|
|
|
name_pattern: str
|
|
domain_pattern: str
|
|
category: str
|
|
vendor: str | None = None
|
|
is_regex: bool = False
|
|
|
|
|
|
@dataclass
|
|
class ClassificationResult:
|
|
"""Result of classifying a cookie."""
|
|
|
|
category: str | None
|
|
vendor: str | None = None
|
|
match_source: str = "unmatched" # exact | wildcard | regex | unmatched
|
|
|
|
|
|
def classify_cookie(
|
|
name: str,
|
|
domain: str,
|
|
patterns: list[KnownPattern],
|
|
) -> ClassificationResult:
|
|
"""Classify a cookie by matching against known patterns.
|
|
|
|
Matching priority:
|
|
1. Exact name match
|
|
2. Wildcard match (patterns containing *)
|
|
3. Regex match (patterns flagged as regex)
|
|
"""
|
|
for pattern in patterns:
|
|
if pattern.is_regex:
|
|
continue # Skip regex in first pass
|
|
|
|
if "*" in pattern.name_pattern:
|
|
# Wildcard match
|
|
regex = pattern.name_pattern.replace(".", r"\.").replace("*", ".*")
|
|
if re.match(f"^{regex}$", name, re.IGNORECASE):
|
|
if _domain_matches(domain, pattern.domain_pattern):
|
|
return ClassificationResult(
|
|
category=pattern.category,
|
|
vendor=pattern.vendor,
|
|
match_source="wildcard",
|
|
)
|
|
elif pattern.name_pattern == name:
|
|
# Exact match
|
|
if _domain_matches(domain, pattern.domain_pattern):
|
|
return ClassificationResult(
|
|
category=pattern.category,
|
|
vendor=pattern.vendor,
|
|
match_source="exact",
|
|
)
|
|
|
|
# Regex pass
|
|
for pattern in patterns:
|
|
if not pattern.is_regex:
|
|
continue
|
|
try:
|
|
if re.match(pattern.name_pattern, name, re.IGNORECASE):
|
|
if _domain_matches(domain, pattern.domain_pattern):
|
|
return ClassificationResult(
|
|
category=pattern.category,
|
|
vendor=pattern.vendor,
|
|
match_source="regex",
|
|
)
|
|
except re.error:
|
|
continue
|
|
|
|
return ClassificationResult(category=None, match_source="unmatched")
|
|
|
|
|
|
def _domain_matches(actual: str, pattern: str) -> bool:
|
|
"""Check if a domain matches a pattern.
|
|
|
|
Patterns can be:
|
|
- "*" — matches any domain
|
|
- ".example.com" — matches example.com and *.example.com
|
|
- "example.com" — exact match
|
|
"""
|
|
if pattern == "*":
|
|
return True
|
|
|
|
actual = actual.lower().lstrip(".")
|
|
pattern = pattern.lower().lstrip(".")
|
|
|
|
if actual == pattern:
|
|
return True
|
|
|
|
# Subdomain match: actual "sub.example.com" matches pattern "example.com"
|
|
if actual.endswith(f".{pattern}"):
|
|
return True
|
|
|
|
return False
|