feat(browser): auto-commit con 178 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
---
|
||||
name: fetch_iab_gvl
|
||||
kind: function
|
||||
lang: py
|
||||
domain: cybersecurity
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def fetch_iab_gvl(out_path: str = \"\", url: str = \"\", lang: str = \"\") -> dict"
|
||||
description: "Descarga y parsea la Global Vendor List (GVL) de IAB Europe TCF: el catalogo maestro de data brokers (vendors) con sus propositos de tratamiento, intereses legitimos, special purposes, features y categorias de datos. Recon de privacidad/tracking."
|
||||
tags: [consent, tcf, gvl, iab, privacy, data-brokers, vendor-list, recon, cmp]
|
||||
params:
|
||||
- name: out_path
|
||||
desc: "Ruta de archivo donde guardar el JSON crudo descargado. Si vacio no guarda nada. Crea los directorios padre si no existen."
|
||||
- name: url
|
||||
desc: "Endpoint de la GVL. Si vacio usa el endpoint TCF v3.2 por defecto (vendor-list.consensu.org/v3/vendor-list.json) y, si falla, hace fallback al v2."
|
||||
- name: lang
|
||||
desc: "Codigo de idioma ISO opcional (ej. es). NO cambia el endpoint principal: las traducciones de propositos viven en endpoints aparte (purposes-<lang>.json). Hoy solo se acepta el parametro; no se descargan traducciones."
|
||||
output: "dict resumen de la GVL. En exito status=ok con versiones (gvlSpecificationVersion, vendorListVersion, tcfPolicyVersion), lastUpdated, contadores (n_vendors, n_purposes, n_specialPurposes, n_features, n_dataCategories) y los mapas vendors / purposes / dataCategories indexados por id (string). En fallo de red o parseo status=error con el mensaje; nunca lanza excepcion."
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_py_core"
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/cybersecurity/fetch_iab_gvl.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join("python", "functions"))
|
||||
from cybersecurity.fetch_iab_gvl import fetch_iab_gvl
|
||||
|
||||
# Descarga real del endpoint v3 (fallback automatico a v2 si falla) y guarda
|
||||
# el JSON crudo para inspeccion posterior.
|
||||
gvl = fetch_iab_gvl(out_path="/tmp/gvl.json")
|
||||
print(gvl["status"]) # ok
|
||||
print(gvl["vendorListVersion"]) # ej. 163
|
||||
print(gvl["n_vendors"]) # > 1000
|
||||
# Mirar un vendor concreto (Google = id 755 en TCF v3)
|
||||
print(gvl["vendors"].get("755", {}).get("name"))
|
||||
```
|
||||
|
||||
Lanzable directo desde la raiz del registry:
|
||||
|
||||
```bash
|
||||
python/.venv/bin/python3 python/functions/cybersecurity/fetch_iab_gvl.py /tmp/gvl.json
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Usala cuando hagas recon de privacidad/tracking de un sitio web y necesites
|
||||
mapear los `vendorId` que aparecen en una cookie de consentimiento (TC String /
|
||||
__tcfapi) a nombres reales de empresas, sus propositos de tratamiento y sus
|
||||
politicas de privacidad. Es el primer paso para auditar quien recibe los datos
|
||||
del usuario via un CMP que implementa el IAB Europe TCF. Tambien para construir
|
||||
un dataset local de data brokers (los `vendors`) y sus declaraciones de datos.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Impura, hace HTTP.** Depende de que `vendor-list.consensu.org` este accesible.
|
||||
En fallo de red o JSON corrupto devuelve `{"status": "error", "error": "..."}`
|
||||
y NO lanza — el caller DEBE comprobar `status` antes de usar el resultado.
|
||||
- **Fallback v3 -> v2.** Si no pasas `url`, intenta v3 y luego v2. Si pasas `url`
|
||||
explicito, solo se intenta esa (sin fallback).
|
||||
- **`policyUrl` derivado.** En GVL v3 los vendors NO tienen un campo `policyUrl`
|
||||
directo; la URL de privacidad vive en `urls[].privacy` (lista por idioma).
|
||||
La funcion la deriva tolerando ambos formatos (v2/v3) y devuelve `""` si no hay.
|
||||
- **`dataCategories` puede faltar** en versiones antiguas (v2). Se tolera la
|
||||
ausencia: `n_dataCategories` sera 0 y el mapa estara vacio.
|
||||
- **`lang` no descarga traducciones.** El parametro existe para la firma futura,
|
||||
pero hoy el resumen siempre viene del endpoint principal (textos en ingles).
|
||||
Las traducciones de propositos estan en endpoints separados
|
||||
(`.../purposes-es.json`) que esta funcion no consulta todavia.
|
||||
- **Payload grande** (~varios MB, >1000 vendors). El dict resumido recorta cada
|
||||
vendor a los campos utiles, pero sigue siendo grande: no lo imprimas entero.
|
||||
@@ -0,0 +1,161 @@
|
||||
"""Descarga y parsea la Global Vendor List (GVL) de IAB Europe TCF.
|
||||
|
||||
La GVL es el catalogo maestro de "data brokers" (vendors) del Transparency &
|
||||
Consent Framework de IAB Europe, con sus propositos de tratamiento de datos,
|
||||
intereses legitimos, special purposes, features y categorias de datos.
|
||||
|
||||
Sin credenciales. Usa solo stdlib (urllib.request) para no anadir dependencias.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
DEFAULT_URL_V3 = "https://vendor-list.consensu.org/v3/vendor-list.json"
|
||||
FALLBACK_URL_V2 = "https://vendor-list.consensu.org/v2/vendor-list.json"
|
||||
|
||||
_USER_AGENT = "fn_registry-fetch_iab_gvl/1.0 (+recon)"
|
||||
_TIMEOUT_S = 30
|
||||
|
||||
|
||||
def _download_json(url: str) -> dict:
|
||||
"""Descarga un JSON via HTTP GET y lo parsea. Lanza en fallo."""
|
||||
req = urllib.request.Request(url, headers={"User-Agent": _USER_AGENT})
|
||||
with urllib.request.urlopen(req, timeout=_TIMEOUT_S) as resp:
|
||||
raw = resp.read()
|
||||
return json.loads(raw.decode("utf-8"))
|
||||
|
||||
|
||||
def _vendor_policy_url(vendor: dict) -> str:
|
||||
"""Deriva la URL de politica de privacidad de un vendor de forma tolerante.
|
||||
|
||||
En GVL v3 los vendors no exponen `policyUrl` directo: la privacy URL vive
|
||||
en `urls[].privacy` (lista por idioma). En v2 algunos vendors si traen
|
||||
`policyUrl`. Esta funcion cubre ambos casos.
|
||||
"""
|
||||
direct = vendor.get("policyUrl")
|
||||
if isinstance(direct, str) and direct:
|
||||
return direct
|
||||
urls = vendor.get("urls") or []
|
||||
if isinstance(urls, list):
|
||||
# Preferir el bloque en ingles si existe; si no, el primero con privacy.
|
||||
for entry in urls:
|
||||
if isinstance(entry, dict) and entry.get("langId") == "en" and entry.get("privacy"):
|
||||
return str(entry["privacy"])
|
||||
for entry in urls:
|
||||
if isinstance(entry, dict) and entry.get("privacy"):
|
||||
return str(entry["privacy"])
|
||||
return ""
|
||||
|
||||
|
||||
def _summarize_vendor(vendor: dict) -> dict:
|
||||
"""Extrae los campos utiles de un vendor, tolerando claves ausentes."""
|
||||
return {
|
||||
"id": vendor.get("id", 0),
|
||||
"name": vendor.get("name", ""),
|
||||
"purposes": vendor.get("purposes", []) or [],
|
||||
"legIntPurposes": vendor.get("legIntPurposes", []) or [],
|
||||
"specialPurposes": vendor.get("specialPurposes", []) or [],
|
||||
"features": vendor.get("features", []) or [],
|
||||
"dataDeclaration": vendor.get("dataDeclaration", []) or [],
|
||||
"policyUrl": _vendor_policy_url(vendor),
|
||||
}
|
||||
|
||||
|
||||
def _summarize_definitions(defs: dict) -> dict:
|
||||
"""Resume un diccionario de definiciones (purposes, dataCategories, ...)."""
|
||||
out: dict = {}
|
||||
for key, item in (defs or {}).items():
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
out[str(key)] = {
|
||||
"id": item.get("id", 0),
|
||||
"name": item.get("name", ""),
|
||||
"description": item.get("description", ""),
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
def fetch_iab_gvl(out_path: str = "", url: str = "", lang: str = "") -> dict:
|
||||
"""Descarga y parsea la Global Vendor List (GVL) de IAB Europe TCF.
|
||||
|
||||
Args:
|
||||
out_path: si no esta vacio, guarda el JSON crudo descargado en esa ruta
|
||||
(crea los directorios padre si hace falta).
|
||||
url: endpoint de la GVL. Si esta vacio usa el endpoint TCF v3.2 por
|
||||
defecto y, si falla, hace fallback al endpoint v2.
|
||||
lang: codigo de idioma ISO opcional (ej. "es"). NO cambia el endpoint
|
||||
principal: las traducciones de propositos viven en endpoints aparte
|
||||
(purposes-<lang>.json). Hoy solo se documenta el parametro; el
|
||||
resumen devuelto sigue siendo el del endpoint principal (ingles).
|
||||
|
||||
Returns:
|
||||
dict con el resumen de la GVL. En exito:
|
||||
{"status": "ok", "gvlSpecificationVersion": ..., "vendorListVersion": ...,
|
||||
"tcfPolicyVersion": ..., "lastUpdated": ..., "n_vendors": int,
|
||||
"n_purposes": int, "n_specialPurposes": int, "n_features": int,
|
||||
"n_dataCategories": int, "vendors": {...}, "purposes": {...},
|
||||
"dataCategories": {...}}.
|
||||
En fallo de red o parseo: {"status": "error", "error": "..."} (no lanza).
|
||||
"""
|
||||
candidates = [url] if url else [DEFAULT_URL_V3, FALLBACK_URL_V2]
|
||||
|
||||
data = None
|
||||
last_error = ""
|
||||
for candidate in candidates:
|
||||
try:
|
||||
data = _download_json(candidate)
|
||||
break
|
||||
except (urllib.error.URLError, urllib.error.HTTPError, ValueError, OSError) as exc:
|
||||
last_error = f"{candidate}: {exc}"
|
||||
continue
|
||||
|
||||
if data is None:
|
||||
return {"status": "error", "error": last_error or "no url candidates"}
|
||||
|
||||
try:
|
||||
if out_path:
|
||||
parent = os.path.dirname(out_path)
|
||||
if parent:
|
||||
os.makedirs(parent, exist_ok=True)
|
||||
with open(out_path, "w", encoding="utf-8") as fh:
|
||||
json.dump(data, fh, ensure_ascii=False)
|
||||
|
||||
vendors_raw = data.get("vendors", {}) or {}
|
||||
purposes_raw = data.get("purposes", {}) or {}
|
||||
special_purposes_raw = data.get("specialPurposes", {}) or {}
|
||||
features_raw = data.get("features", {}) or {}
|
||||
data_categories_raw = data.get("dataCategories", {}) or {}
|
||||
|
||||
vendors = {str(vid): _summarize_vendor(v) for vid, v in vendors_raw.items()}
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"gvlSpecificationVersion": data.get("gvlSpecificationVersion"),
|
||||
"vendorListVersion": data.get("vendorListVersion"),
|
||||
"tcfPolicyVersion": data.get("tcfPolicyVersion"),
|
||||
"lastUpdated": data.get("lastUpdated"),
|
||||
"n_vendors": len(vendors_raw),
|
||||
"n_purposes": len(purposes_raw),
|
||||
"n_specialPurposes": len(special_purposes_raw),
|
||||
"n_features": len(features_raw),
|
||||
"n_dataCategories": len(data_categories_raw),
|
||||
"vendors": vendors,
|
||||
"purposes": _summarize_definitions(purposes_raw),
|
||||
"dataCategories": _summarize_definitions(data_categories_raw),
|
||||
}
|
||||
except Exception as exc: # noqa: BLE001 - contrato: nunca lanzar.
|
||||
return {"status": "error", "error": str(exc)}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
result = fetch_iab_gvl(out_path=sys.argv[1] if len(sys.argv) > 1 else "")
|
||||
print(json.dumps(
|
||||
{k: v for k, v in result.items() if k not in ("vendors", "purposes", "dataCategories")},
|
||||
indent=2,
|
||||
))
|
||||
if result.get("status") == "ok":
|
||||
print(f"sample vendors: {list(result['vendors'].items())[:1]}")
|
||||
Reference in New Issue
Block a user