feat(browser): auto-commit con 178 cambios

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-20 18:22:23 +02:00
parent 7d100e7f3e
commit 763e06c127
178 changed files with 19917 additions and 317 deletions
@@ -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]}")