Herramienta educativa de análisis de exposición de la privacidad: mediante OAuth con tus propias cuentas (Instagram), analiza tu huella digital pública, infiere atributos demográficos y calcula el estrechamiento de anonimato frente a datos de referencia del INE, generando un informe de privacidad con IA.
Python
1
240 commits
updated Oct 7, 2026
Base de este proyecto: plantilla de laboratorio ASW (Uniovi)
yovi_en1b. Se ha adaptado sustituyendo el dominio (usuarios/juego → análisis de exposición de identidad digital) y sustituyendo el serviciousersde Node.js por un backend Python/FastAPI (necesario por las librerías de NLP: spaCy, scikit-learn, DINOv2/FAISS). El serviciogamey(Rust) se ha eliminado por no aplicar a este TFG.
https://github.com/user-attachments/assets/bc91d3e4-bbb0-44b1-be25-00c0aba36d51
TFG: análisis defensivo de la propia huella digital mediante OSINT e IA. El usuario autentica su propia cuenta de Reddit y/o Instagram vía OAuth, y la herramienta genera un informe con lo que es públicamente inferible sobre él — para que decida qué quiere seguir compartiendo, no para vigilar a terceros.
Todo el pipeline corre en memoria durante la petición: no hay base de datos, no se persiste nada del usuario más allá de la sesión de su propio navegador.
webapp/ — frontend React + Vite + TypeScript. Landing (consentimiento
backend/ — backend Python + FastAPI (renombrado desde users/,
nombre heredado de la plantilla). Aquí vive toda la lógica de OAuth, NLP,
k-anonimato, geolocalización por imagen, scoring y el informe (ver
backend/app/).docs/ — documentación de arquitectura Arc42.graphify-out/ — grafo de conocimiento del código, ver sección "Graphify" más abajo.SessionMiddleware) con los tokens de acceso. Cerrar sesión =
borrar todo rastro.backend/app/scoring/k_anonymity.py) usa
distribuciones agregadas del INE y asume independencia entre
atributos (no microdatos reales, no correlaciones cruzadas) — es una
aproximación documentada, no un conteo exacto. Ver el docstring del
módulo para la justificación de diseño frente a la alternativa
descartada (base de datos sintética de ~49M filas).backend/app/vision/geolocation.py) es
opcional y best-effort: si el índice FAISS no está construido, o si
el backend no tiene instaladas sus dependencias pesadas (torch,
faiss, transformers -- ver el extra vision de pyproject.toml y el ARG WITH_GEOLOCATION del Dockerfile), esa función del pipeline
simplemente no aporta nada, sin romper el resto del análisis. Su
precisión realista es a nivel de provincia, no de calle (ver benchmarks
de reverse geolocation citados en los docstrings).backend/app/vision/scene_analysis.py,
Moondream2) es igualmente opcional y best-effort, y depende de las
mismas condiciones de activación que la geolocalización (mismo
el extra vision de pyproject.toml, mismo ARG WITH_GEOLOCATION del
Dockerfile) aunque desde el cambio a llama-cpp-python/GGUF ya NO
instala las mismas dependencias de Python que la geolocalización (antes
sí, ambas usaban transformers). Es arquitectónicamente distinto: no
compara similitud contra un índice, sino que "interpreta" cada foto vía
VQA. Se precarga en el arranque del contenedor igual que el modelo de
geolocalización (ver lifespan en app/main.py).scene_analysis.py como señal adicional para
acotar el k-anonimato (misma lógica que ya se aplica a atributos
autodeclarados en texto), pero se descartó por dos motivos, no solo uno:
scene_analysis.py se mantiene deliberadamente
acotado a objetos/escena/aficiones/indicios de pareja -- nunca a la
descripción física de las personas que aparecen.backend/app/nlp/attribute_inference.py,
backend/app/nlp/demographic_extraction.py) son deliberadamente simples
(listas + regex) para mantener el sistema explicable y auditable.backend/app/ai_analysis.py,
backend/app/nlp/ai_attribute_extraction.py) es totalmente opcional:
ambos usan el mismo modelo local (Qwen3.5-4B, ver
backend/app/nlp/ai_client.py). Sin QWEN_GGUF_REPO_ID configurado (o
sin llama-cpp-python instalado), el veredicto/conclusiones del
dashboard indican que no está disponible, y la detección de atributos se
queda solo con lo que encuentren las regex -- el resto de la app
funciona igual.identity_consistency_risk del scoring quedan como
trabajo futuro, documentado explícitamente en
backend/app/scoring/privacy_score.py.webapp/ — SPA con Vite + React/TypeScript: pantalla de consentimiento y login OAuth, dashboard con el informe completo (score, progreso en vivo vía SSE, estrechamiento de población, mapa, gráficos, veredicto de IA), tests con Vitest/Testing Library y E2E con Playwright + Cucumber.backend/ — API en Python/FastAPI: OAuth con cada plataforma, extracción y normalización de posts públicos, inferencia de atributos (regex + IA opcional), estrechamiento de k-anonimato contra datos del INE, scoring de privacidad, geolocalización de fotos (DINOv2 + FAISS, opcional) y análisis de escena (Moondream2, opcional), generación del informe final y veredicto de IA.webapp/)src/pages/Landing.tsx — pantalla de consentimiento + login OAuth (Reddit/Instagram).src/pages/Dashboard.tsx — informe completo: score, progreso en vivo vía SSE, tabla de estrechamiento de población (con pictograma visual), mapa + lista de ubicaciones estimadas, gráfico horario, perfil de escritura, veredicto y conclusiones de IA, descarga en JSON.src/components/ — ScoreBar, HourlyActivityChart, PopulationNarrowingTable, PopulationPictogram (representación visual tipo isotype: monigotes turquesa/negro), LocationMap (Leaflet/OpenStreetMap), AiSummaryCard, DownloadReportButton.src/api.ts / src/types.ts — cliente tipado del backend, incluido analyzeStream (SSE vía EventSource).src/utils/reportToJson.ts — exportación del informe a JSON.src/__tests__/), E2E con Playwright + Cucumber (webapp/test/, ver webapp/E2E.md).backend/)app/auth/reddit_oauth.py, app/auth/instagram_oauth.py — OAuth 2.0 con cada plataforma.app/auth/dynamic_origin.py — deriva el redirect_uri de Instagram y el destino de la redirección final del Host de cada petición cuando no hay un valor fijo en .env (pensado para túneles HTTPS tipo ngrok, cuya URL cambia en cada reinicio; ver nota más abajo sobre por qué no usar Cloudflare Quick Tunnel).app/reddit_client.py, app/instagram_client.py — extracción de posts/comentarios/publicaciones públicas, normalizados a un modelo común (SocialPost).app/nlp/fingerprint.py — huella de escritura (longitud de frase, vocabulario, emojis, patrón horario, keywords TF-IDF, idioma).app/nlp/attribute_inference.py — inferencia explicable de atributos (ubicación, ocupación, rutina) a partir de comunidades/hashtags.app/nlp/demographic_extraction.py — extracción de declaraciones explícitas en texto por regex (edad, sexo, ubicación, estudios, ocupación, universidad, empresa).app/nlp/ai_attribute_extraction.py — la misma extracción, pero vía IA local (Qwen3.5-4B, opcional): capta redacciones libres que la regex no reconoce, y una estimación aparte de sexo por nombre público de la cuenta (marcada con menor fiabilidad). Complementa a la regex, nunca la sustituye.app/nlp/travel_detection.py — detección regex de menciones de "de viaje/vacaciones" en el pie de foto, para excluir esas publicaciones del cálculo de dónde vives habitualmente (ver report/generator.py). Complementa (no sustituye) a una detección equivalente vía IA en ai_attribute_extraction.py.app/data/ine_reference.py — tablas de distribución poblacional (INE) usadas para el estrechamiento de población.app/scoring/k_anonymity.py — motor de estimación de k-anonimato (estrechamiento de población en cascada), expone también la proporción ya calculada para el pictograma del frontend.app/scoring/privacy_score.py — motor de scoring de privacidad (0-100).app/vision/geolocation.py — geolocalización de fotos por similitud visual (DINOv2 + FAISS), opcional. Devuelve todas las estimaciones (con su confianza real) más un flag de si el índice está disponible, para poder distinguir "no hay índice" de "no hay resultados fiables".app/vision/scene_analysis.py — análisis del contenido visual de cada foto (Moondream2, modelo de visión-lenguaje local vía llama-cpp-python/GGUF, ~1.4B parámetros de texto): objetos, actividades, aficiones, señales de relación de pareja, matrícula. Arquitectónicamente distinto de geolocation.py (que compara similitud visual contra un índice sin "entender" la foto). Opcional, mismo flag de activación que la geolocalización (WITH_GEOLOCATION), aunque ya no comparte sus mismas dependencias de Python (ver más abajo).app/ai_analysis.py — veredicto general + conclusiones sobre el informe vía IA local (Qwen3.5-4B), opcional; se dispara automáticamente, sin botón, y usa recommendations como insumo.app/progress.py — callback de progreso compartido, usado por el endpoint de streaming.app/analysis_router.py — endpoints de análisis (/api/analyze/{platform}, /api/analyze/{platform}/stream, /api/analyze/ai-summary).app/report/generator.py — ensamblado del informe final + recomendaciones (estas últimas ya no se muestran como sección propia en el dashboard, ver más arriba).app/main.py — app FastAPI; en el arranque (lifespan) precarga el modelo de geolocalización si está disponible, en vez de esperar al primer análisis; métricas Prometheus en /metrics.tests/ — pytest (unit + endpoints), ~95% cobertura, para Sonar.scripts/ — descarga del dataset OSV-5M, construcción del índice FAISS, y recuperación de metadatos (ver más abajo).monitoring/ — configuración de Prometheus/Grafana.docker compose up --build
Arranca solo backend + webapp (servicios sin perfil):
/docs)prometheus/grafana (perfil monitoring) y dinov2-igpu-worker
(perfil igpu, ver más abajo) son opt-in por diseño -- no arrancan con
el comando de arriba a secas, salvo que actives el perfil correspondiente
por defecto (ver justo debajo):
docker compose --profile monitoring up --build # + Grafana/Prometheus
docker compose --profile igpu up --build # + worker de iGPU
# combinables:
docker compose --profile monitoring --profile igpu up --build
Activar un perfil por defecto, sin pasar --profile cada vez: copia
.env.example (el de la raíz del repo, junto a este docker-compose.yml
-- distinto de backend/.env.example) a .env y pon ahí
COMPOSE_PROFILES=igpu (combinable: COMPOSE_PROFILES=igpu,monitoring).
Compose lee ese .env de la raíz automáticamente, así que a partir de
ahí docker compose up --build a secas ya arranca también
dinov2-igpu-worker -- sin el .env de la raíz (o sin esa variable en
él), sigue haciendo falta el flag --profile explícito. Sirve para
cualquier perfil, no solo igpu.
--profile monitoring, activo o no por defecto)Antes de levantarlo, crea backend/.env a partir de backend/.env.example
(ver variables de entorno más abajo).
Análisis de imagen en Docker (geolocalización + contenido visual): el
docker-compose.yml trae WITH_GEOLOCATION=true por defecto, que instala
torch/faiss/transformers (para DINOv2, geolocalización) y
llama-cpp-python compilado con soporte CUDA (para Moondream2, análisis
de contenido -- ver el extra vision de pyproject.toml y la etapa cuda-builder
del Dockerfile) en la imagen del backend, varios cientos de MB extra.
Pese al nombre del flag (heredado de cuando solo existía
geolocalización), esa misma build es la que necesita también el análisis
de contenido visual (app/vision/scene_analysis.py, Moondream2 vía GGUF)
-- no hay un flag independiente para activar solo una de las dos, aunque
desde el cambio a llama-cpp-python ya NO comparten las mismas
dependencias de Python (antes sí, ambas usaban transformers). Sin esto,
el backend puede tener el índice FAISS perfectamente construido y montado
y aun así reportar la geolocalización como "no disponible" -- esas
librerías hacen falta en el análisis, no solo para construir el índice.
Si no vas a usar ninguna de las dos funciones, ponlo a false para una
imagen más ligera. Los dos modelos (DINOv2 y Moondream2) se precargan en
el arranque del contenedor, no en el primer análisis (ver lifespan en
app/main.py). La caché de ambos se persiste en backend/data/hf_cache/
para no volver a descargarlos en cada reinicio; Moondream2 además cachea
ahí su versión cuantizada (ver MOONDREAM_QUANT_TYPE en
app/vision/scene_analysis.py) una vez generada, para no recuantizar en
cada reinicio.
Offload de DINOv2 a una iGPU (opcional, --profile igpu): en
máquinas con GPU dedicada + iGPU (Intel/AMD, vía DirectML sobre WSL2),
dinov2-igpu-worker descarga su propio torch==2.4.1+cu121 +
torch-directml -- deliberadamente en una imagen/venv completamente
aparte del backend, nunca instalado ahí (ver ADR-28 en
docs/src/09_architecture_decisions.adoc para el porqué: mezclarlo con
el torch/cu121 que ya usa el backend para DINOv2 corrompe ese
entorno). Actívalo con ENABLE_IGPU_OFFLOAD=true en backend/.env y
el perfil igpu al arrancar (a mano con --profile igpu, o por defecto
con COMPOSE_PROFILES=igpu en el .env de la raíz -- ver más arriba) --
ninguno de los dos por separado hace nada.
Construir backend + webapp + dinov2-igpu-worker los tres a la vez
(cada uno con su propia descarga de torch) puede saturar los recursos
de Docker Desktop/WSL2 y el build del worker termina con un
rpc error: code = Unavailable desc = error reading from server: EOF
justo al final (paso exporting layers) aunque los otros dos hayan
terminado bien. Si te pasa:
docker images | findstr amitraceable # confirma si backend/webapp sí terminaron
docker compose build dinov2-igpu-worker # reintenta solo el que falló
Si vuelve a fallar igual, es contención de recursos, no mala suerte puntual -- en orden de probabilidad:
wsl --shutdown y reabrir Docker Desktop (el demonio de BuildKit a
veces queda colgado tras un fallo así).%UserProfile%\.wslconfig:
[wsl2]
memory=8GB
y wsl --shutdown para aplicarlo.docker compose build backend
docker compose build webapp
docker compose --profile igpu build dinov2-igpu-worker
docker system df) -- los dos torch+cu121
(backend y worker, cada uno de varios cientos de MB/GB en wheels) más
las capas intermedias de build acumulan bastante; docker builder prune libera caché de builds antiguos sin tocar las imágenes en uso.backend/)cd backend
uv sync # crea .venv con las dependencias, el grupo dev y los modelos de spaCy
cp .env.example .env # y rellenar credenciales
uv run uvicorn app.main:app --reload --port 3000
(Requiere uv; descarga
solo el Python 3.12 que fija backend/pyproject.toml.)
Si quieres geolocalización por imagen o análisis de contenido visual (Moondream2) funcionando en este venv, hace falta un paso más -- lo de arriba, por sí solo, deja ambas funciones en "no disponible" de forma silenciosa (ver más abajo, "Alcance y limitaciones"): no falla con un error, simplemente esa parte del pipeline no aporta nada, así que es fácil no darse cuenta de que falta este paso.
uv sync --extra vision
Con Docker esto ya viene resuelto por defecto (ver WITH_GEOLOCATION más
abajo) -- este paso solo es necesario si ejecutas el backend directamente
con uvicorn, como aquí arriba.
En arranques posteriores, con el venv ya creado, basta con activar el entorno y lanzar uvicorn directamente.
cd webapp
npm install
npm run dev
La webapp estará en http://localhost:5173
Ver backend/.env.example para la lista completa comentada. Resumen:
| Variable | Obligatoria | Notas |
|---|---|---|
REDDIT_CLIENT_ID, REDDIT_CLIENT_SECRET, REDDIT_REDIRECT_URI, REDDIT_USER_AGENT | Sí | App tipo "web app" en https://www.reddit.com/prefs/apps |
SESSION_SECRET_KEY | Sí | Cadena aleatoria larga, firma la cookie de sesión |
INSTAGRAM_APP_ID, INSTAGRAM_APP_SECRET | No | Sin ellas, Instagram devuelve 503 pero Reddit sigue funcionando. App "API setup with Instagram Login" en Meta for Developers |
INSTAGRAM_REDIRECT_URI | No | Déjala vacía si usas Docker (ver nota de túnel más abajo): se deriva sola del Host de cada petición. Fíjala solo con dominio propio en producción |
FRONTEND_ORIGIN | No | Déjala vacía si usas Docker: se deriva sola del Host. Fíjala (p. ej. http://localhost:5173) solo si sirves frontend y backend en puertos distintos sin Docker |
ENABLE_AI_ANALYSIS | No | true por defecto. Desactiva del todo el análisis con IA (veredicto/conclusiones + extracción de atributos, ver backend/app/nlp/ai_client.py) sin desinstalar nada |
QWEN_GGUF_REPO_ID, QWEN_GGUF_FILENAME | No | Repo/fichero de Hugging Face con el GGUF de Qwen3.5-4B a cuantizar localmente (Q4_K_M vía llama-cpp-python) -- SIN VALOR POR DEFECTO A PROPÓSITO, hay que confirmarlo contra lo disponible en Hugging Face antes de desplegar (ver .env.example). Sin rellenar, la sección "Analizar con IA" del dashboard indica que no está disponible, sin afectar al resto |
ENABLE_SCENE_ANALYSIS | No | false por defecto. Activa el análisis de contenido visual con Moondream2 (descripción de escena por foto). Afecta exclusivamente a esto: la geolocalización por similitud visual (DINOv2) no lee esta variable y sigue funcionando igual, esté esto activado o no -- probado explícitamente en test_geolocation.py. Para que Moondream2 pueda funcionar (con esta variable en true) hace falta además WITH_GEOLOCATION=true en el build de Docker (mismo build-arg, aunque desde ADR-43 instala llama-cpp-python para Moondream2, no las mismas librerías que DINOv2). Ver ADR-43 en docs/ (sustituye a ADR-19, que documentaba el backend transformers anterior): la cifra de RAM/VRAM necesaria depende ahora de si MOONDREAM_QUANT_TYPE está cuantizando el modelo o no, no hay un único número fijo como antes |
Nota sobre Instagram y HTTPS en local: la API de Instagram (Business
Login) exige que redirect_uri sea HTTPS, incluso en desarrollo. Para
probarlo en local sin dominio propio, usa ngrok apuntando a la
webapp (puerto 8080), no al backend directamente -- la webapp lleva
su propio nginx que hace de proxy hacia el backend bajo el mismo origen
(ver webapp/nginx.conf), así que un único túnel sirve para todo:
ngrok http 8080
(requiere cuenta gratuita en https://ngrok.com y ngrok config add-authtoken <token> una vez).
Restringir quién puede entrar (Basic Auth): el plan gratuito de ngrok
incluye un dominio propio estable (no cambia en cada reinicio) y
permite añadir autenticación sin plan de pago, vía Traffic Policy.
Copia backend/policy.example.yaml a backend/policy.yaml (este
segundo nombre está en .gitignore, nunca se sube) y pon tu
usuario/contraseña real dentro:
on_http_request:
- actions:
- type: basic-auth
config:
realm: "AmITraceable"
credentials:
- "usuario:contraseña-larga-y-unica"
Y arranca el túnel apuntando a ese archivo:
ngrok http 8080 --url=tu-dominio.ngrok-free.app --traffic-policy-file=backend/policy.yaml
Así, quien no tenga esas credenciales ni siquiera llega al backend -- ngrok rechaza la petición en su propio borde, antes de reenviarla. Ver ADR-39 sobre por qué esto (y no dominio propio + Cloudflare) es la solución elegida.
⚠️ No usar cloudflared tunnel --url ... (Cloudflare Quick Tunnel):
el edge de trycloudflare.com bufferiza por completo las respuestas
text/event-stream, así que el streaming SSE de /api/analyze/{platform}/stream
(la pantalla de carga con el listado de fases) nunca llega en tiempo real
al navegador -- todos los eventos se reciben de golpe justo cuando el
análisis termina, y se ve solo "Analizando tu actividad pública en
X..." sin el listado de fases mientras carga. Es un problema conocido y
documentado del propio Cloudflare (no del backend: las cabeceras
text/event-stream y X-Accel-Buffering: no ya se mandan bien). Un
named tunnel de Cloudflare (con dominio propio) sí respetaría el
streaming, pero para desarrollo local sin dominio, ngrok funciona
correctamente sin ese problema.
Con INSTAGRAM_REDIRECT_URI y FRONTEND_ORIGIN vacías en .env (valor
por defecto), lo único que hay que hacer con la URL https://xxx.ngrok-free.app
que te dé cada vez es darla de alta en la app de Meta Developers (con el
path /auth/instagram/callback) -- eso sigue siendo manual, Meta no tiene
API para gestionar esa lista. Todo lo demás (.env, reconstruir imágenes,
reiniciar uvicorn) se adapta solo en cada reinicio del túnel.
La primera vez que abras la URL de ngrok en el navegador, el plan gratuito muestra una página intermedia de aviso ("You are about to visit...") -- solo hay que darle a "Visit Site", no es un error.
El módulo de geolocalización de fotos (app/vision/geolocation.py) es
opcional: sin el índice FAISS construido, simplemente no aporta nada al
informe, sin errores. Para activarlo:
cd backend
uv sync --extra vision
uv run python scripts/geolocalization/download_osv5m_spain.py --output data/osv5m_spain --max-disk-gb 35
uv run python scripts/geolocalization/build_faiss_index.py --images data/osv5m_spain
(El extra vision apunta al índice de PyTorch con CUDA 12.1
(pytorch-cu121, ver [tool.uv.sources] en backend/pyproject.toml) -- ver
también el ARG WITH_GEOLOCATION del Dockerfile si vas a correr esto
dentro de Docker.)
El análisis de contenido visual (app/vision/scene_analysis.py,
Moondream2) usa el mismo el extra vision de pyproject.toml, pero no necesita nada
de lo anterior: no hay índice que construir ni dataset que descargar, se
ejecuta directamente sobre cada foto.
download_osv5m_spain.py descarga solo las imágenes de España del
dataset OpenStreetView-5M
(streaming shard a shard, con límite de disco configurable, reanudable
tras interrupción).download_osv5m_world.py es la variante sin filtro de país (mucho más
pesada en tráfico de red, ~260GB).build_faiss_index.py extrae embeddings con DINOv2 y construye el
índice de búsqueda por similitud.recover_metadata.py reconstruye metadata.csv a partir de las
imágenes ya descargadas, sin volver a bajarlas -- pensado para el caso
(ya corregido, pero por si tienes un metadata.csv de una ejecución
anterior al fix) en que se perdía la columna id al guardar, dejando el
índice sin poder casar ninguna imagen con su fila de metadatos.Estos datos/artefactos no se versionan en el repositorio (ver
.gitignore): son regenerables ejecutando los scripts.
webapp/package.json)npm run dev — servidor de desarrollo.npm test — tests unitarios (Vitest, ~100 tests).npm run test:coverage — tests con cobertura (para Sonar).npm run test:e2e — tests E2E (levanta webapp + backend sueltos y corre Cucumber; ver webapp/E2E.md). Cubre la pantalla de consentimiento y que el dashboard exige autenticación real contra el backend (sin mockear), pero no el flujo completo de OAuth (necesitaría credenciales reales) ni el montaje con Docker/nginx.npm run start:all — levanta webapp + backend Python a la vez (conveniencia para desarrollo/E2E).npm run lint — ESLint.uv run uvicorn app.main:app --reload --port 3000 — arranca el backend en desarrollo.uv run pytest — tests unitarios (~260 tests, 1 se salta si no tienes instalado el extra vision en local).uv run pytest --cov=app --cov-report=xml --cov-report=term — tests con cobertura (genera coverage.xml para Sonar).El repo tiene un grafo generado con Graphify
(graphify-out/) que Claude Code consulta automáticamente en vez de releer archivos
sueltos, ahorrando tokens. Solo funciona así de forma automática dentro de Claude Code
(usa hooks internos de esa herramienta) — en un chat normal de Claude no hay ahorro
automático salvo que se le pida explícitamente consultar el grafo por terminal.
Al clonar en un equipo nuevo, ejecutar una vez (regeneran configuración local, no se versiona):
uv tool install graphifyy # o: pipx install graphifyy — evitar "pip" a secas
graphify claude install --project # regenera .claude/settings.json con la ruta de este PC
graphify hook install # auto-actualiza el grafo tras cada commit
graphify-out/ (grafo, informe, visualización) sí se versiona; .claude/settings.json,
graphify-out/cache/, graphify-out/cost.json y las carpetas de backup con fecha
(graphify-out/20YY-MM-DD/) no, porque son específicos de cada máquina — ver .gitignore.
Debido a restricciones de la cuenta gratuita, el token de Sonar debe
renovarse cada dos meses. Configuración en sonar-project.properties
(rutas de fuentes/tests, exclusiones, reportes de cobertura).
No incluido en el código, pero necesario antes de la defensa:
_WEIGHTS en
privacy_score.py) y de la asunción de independencia del estrechamiento
de población (k_anonymity.py) contra percepción subjetiva de usuarios
reales o correlaciones reales del Censo/EPA.Herramienta educativa de análisis de exposición de la privacidad: mediante OAuth con tus propias cuentas (Instagram), analiza tu huella digital pública, infiere atributos demográficos y calcula el estrechamiento de anonimato frente a datos de referencia del INE, generando un informe de privacidad con IA.
Python
1
240 commits
updated Oct 7, 2026
Base de este proyecto: plantilla de laboratorio ASW (Uniovi)
yovi_en1b. Se ha adaptado sustituyendo el dominio (usuarios/juego → análisis de exposición de identidad digital) y sustituyendo el serviciousersde Node.js por un backend Python/FastAPI (necesario por las librerías de NLP: spaCy, scikit-learn, DINOv2/FAISS). El serviciogamey(Rust) se ha eliminado por no aplicar a este TFG.
https://github.com/user-attachments/assets/bc91d3e4-bbb0-44b1-be25-00c0aba36d51
TFG: análisis defensivo de la propia huella digital mediante OSINT e IA. El usuario autentica su propia cuenta de Reddit y/o Instagram vía OAuth, y la herramienta genera un informe con lo que es públicamente inferible sobre él — para que decida qué quiere seguir compartiendo, no para vigilar a terceros.
Todo el pipeline corre en memoria durante la petición: no hay base de datos, no se persiste nada del usuario más allá de la sesión de su propio navegador.
webapp/ — frontend React + Vite + TypeScript. Landing (consentimiento
backend/ — backend Python + FastAPI (renombrado desde users/,
nombre heredado de la plantilla). Aquí vive toda la lógica de OAuth, NLP,
k-anonimato, geolocalización por imagen, scoring y el informe (ver
backend/app/).docs/ — documentación de arquitectura Arc42.graphify-out/ — grafo de conocimiento del código, ver sección "Graphify" más abajo.SessionMiddleware) con los tokens de acceso. Cerrar sesión =
borrar todo rastro.backend/app/scoring/k_anonymity.py) usa
distribuciones agregadas del INE y asume independencia entre
atributos (no microdatos reales, no correlaciones cruzadas) — es una
aproximación documentada, no un conteo exacto. Ver el docstring del
módulo para la justificación de diseño frente a la alternativa
descartada (base de datos sintética de ~49M filas).backend/app/vision/geolocation.py) es
opcional y best-effort: si el índice FAISS no está construido, o si
el backend no tiene instaladas sus dependencias pesadas (torch,
faiss, transformers -- ver el extra vision de pyproject.toml y el ARG WITH_GEOLOCATION del Dockerfile), esa función del pipeline
simplemente no aporta nada, sin romper el resto del análisis. Su
precisión realista es a nivel de provincia, no de calle (ver benchmarks
de reverse geolocation citados en los docstrings).backend/app/vision/scene_analysis.py,
Moondream2) es igualmente opcional y best-effort, y depende de las
mismas condiciones de activación que la geolocalización (mismo
el extra vision de pyproject.toml, mismo ARG WITH_GEOLOCATION del
Dockerfile) aunque desde el cambio a llama-cpp-python/GGUF ya NO
instala las mismas dependencias de Python que la geolocalización (antes
sí, ambas usaban transformers). Es arquitectónicamente distinto: no
compara similitud contra un índice, sino que "interpreta" cada foto vía
VQA. Se precarga en el arranque del contenedor igual que el modelo de
geolocalización (ver lifespan en app/main.py).scene_analysis.py como señal adicional para
acotar el k-anonimato (misma lógica que ya se aplica a atributos
autodeclarados en texto), pero se descartó por dos motivos, no solo uno:
scene_analysis.py se mantiene deliberadamente
acotado a objetos/escena/aficiones/indicios de pareja -- nunca a la
descripción física de las personas que aparecen.backend/app/nlp/attribute_inference.py,
backend/app/nlp/demographic_extraction.py) son deliberadamente simples
(listas + regex) para mantener el sistema explicable y auditable.backend/app/ai_analysis.py,
backend/app/nlp/ai_attribute_extraction.py) es totalmente opcional:
ambos usan el mismo modelo local (Qwen3.5-4B, ver
backend/app/nlp/ai_client.py). Sin QWEN_GGUF_REPO_ID configurado (o
sin llama-cpp-python instalado), el veredicto/conclusiones del
dashboard indican que no está disponible, y la detección de atributos se
queda solo con lo que encuentren las regex -- el resto de la app
funciona igual.identity_consistency_risk del scoring quedan como
trabajo futuro, documentado explícitamente en
backend/app/scoring/privacy_score.py.webapp/ — SPA con Vite + React/TypeScript: pantalla de consentimiento y login OAuth, dashboard con el informe completo (score, progreso en vivo vía SSE, estrechamiento de población, mapa, gráficos, veredicto de IA), tests con Vitest/Testing Library y E2E con Playwright + Cucumber.backend/ — API en Python/FastAPI: OAuth con cada plataforma, extracción y normalización de posts públicos, inferencia de atributos (regex + IA opcional), estrechamiento de k-anonimato contra datos del INE, scoring de privacidad, geolocalización de fotos (DINOv2 + FAISS, opcional) y análisis de escena (Moondream2, opcional), generación del informe final y veredicto de IA.webapp/)src/pages/Landing.tsx — pantalla de consentimiento + login OAuth (Reddit/Instagram).src/pages/Dashboard.tsx — informe completo: score, progreso en vivo vía SSE, tabla de estrechamiento de población (con pictograma visual), mapa + lista de ubicaciones estimadas, gráfico horario, perfil de escritura, veredicto y conclusiones de IA, descarga en JSON.src/components/ — ScoreBar, HourlyActivityChart, PopulationNarrowingTable, PopulationPictogram (representación visual tipo isotype: monigotes turquesa/negro), LocationMap (Leaflet/OpenStreetMap), AiSummaryCard, DownloadReportButton.src/api.ts / src/types.ts — cliente tipado del backend, incluido analyzeStream (SSE vía EventSource).src/utils/reportToJson.ts — exportación del informe a JSON.src/__tests__/), E2E con Playwright + Cucumber (webapp/test/, ver webapp/E2E.md).backend/)app/auth/reddit_oauth.py, app/auth/instagram_oauth.py — OAuth 2.0 con cada plataforma.app/auth/dynamic_origin.py — deriva el redirect_uri de Instagram y el destino de la redirección final del Host de cada petición cuando no hay un valor fijo en .env (pensado para túneles HTTPS tipo ngrok, cuya URL cambia en cada reinicio; ver nota más abajo sobre por qué no usar Cloudflare Quick Tunnel).app/reddit_client.py, app/instagram_client.py — extracción de posts/comentarios/publicaciones públicas, normalizados a un modelo común (SocialPost).app/nlp/fingerprint.py — huella de escritura (longitud de frase, vocabulario, emojis, patrón horario, keywords TF-IDF, idioma).app/nlp/attribute_inference.py — inferencia explicable de atributos (ubicación, ocupación, rutina) a partir de comunidades/hashtags.app/nlp/demographic_extraction.py — extracción de declaraciones explícitas en texto por regex (edad, sexo, ubicación, estudios, ocupación, universidad, empresa).app/nlp/ai_attribute_extraction.py — la misma extracción, pero vía IA local (Qwen3.5-4B, opcional): capta redacciones libres que la regex no reconoce, y una estimación aparte de sexo por nombre público de la cuenta (marcada con menor fiabilidad). Complementa a la regex, nunca la sustituye.app/nlp/travel_detection.py — detección regex de menciones de "de viaje/vacaciones" en el pie de foto, para excluir esas publicaciones del cálculo de dónde vives habitualmente (ver report/generator.py). Complementa (no sustituye) a una detección equivalente vía IA en ai_attribute_extraction.py.app/data/ine_reference.py — tablas de distribución poblacional (INE) usadas para el estrechamiento de población.app/scoring/k_anonymity.py — motor de estimación de k-anonimato (estrechamiento de población en cascada), expone también la proporción ya calculada para el pictograma del frontend.app/scoring/privacy_score.py — motor de scoring de privacidad (0-100).app/vision/geolocation.py — geolocalización de fotos por similitud visual (DINOv2 + FAISS), opcional. Devuelve todas las estimaciones (con su confianza real) más un flag de si el índice está disponible, para poder distinguir "no hay índice" de "no hay resultados fiables".app/vision/scene_analysis.py — análisis del contenido visual de cada foto (Moondream2, modelo de visión-lenguaje local vía llama-cpp-python/GGUF, ~1.4B parámetros de texto): objetos, actividades, aficiones, señales de relación de pareja, matrícula. Arquitectónicamente distinto de geolocation.py (que compara similitud visual contra un índice sin "entender" la foto). Opcional, mismo flag de activación que la geolocalización (WITH_GEOLOCATION), aunque ya no comparte sus mismas dependencias de Python (ver más abajo).app/ai_analysis.py — veredicto general + conclusiones sobre el informe vía IA local (Qwen3.5-4B), opcional; se dispara automáticamente, sin botón, y usa recommendations como insumo.app/progress.py — callback de progreso compartido, usado por el endpoint de streaming.app/analysis_router.py — endpoints de análisis (/api/analyze/{platform}, /api/analyze/{platform}/stream, /api/analyze/ai-summary).app/report/generator.py — ensamblado del informe final + recomendaciones (estas últimas ya no se muestran como sección propia en el dashboard, ver más arriba).app/main.py — app FastAPI; en el arranque (lifespan) precarga el modelo de geolocalización si está disponible, en vez de esperar al primer análisis; métricas Prometheus en /metrics.tests/ — pytest (unit + endpoints), ~95% cobertura, para Sonar.scripts/ — descarga del dataset OSV-5M, construcción del índice FAISS, y recuperación de metadatos (ver más abajo).monitoring/ — configuración de Prometheus/Grafana.docker compose up --build
Arranca solo backend + webapp (servicios sin perfil):
/docs)prometheus/grafana (perfil monitoring) y dinov2-igpu-worker
(perfil igpu, ver más abajo) son opt-in por diseño -- no arrancan con
el comando de arriba a secas, salvo que actives el perfil correspondiente
por defecto (ver justo debajo):
docker compose --profile monitoring up --build # + Grafana/Prometheus
docker compose --profile igpu up --build # + worker de iGPU
# combinables:
docker compose --profile monitoring --profile igpu up --build
Activar un perfil por defecto, sin pasar --profile cada vez: copia
.env.example (el de la raíz del repo, junto a este docker-compose.yml
-- distinto de backend/.env.example) a .env y pon ahí
COMPOSE_PROFILES=igpu (combinable: COMPOSE_PROFILES=igpu,monitoring).
Compose lee ese .env de la raíz automáticamente, así que a partir de
ahí docker compose up --build a secas ya arranca también
dinov2-igpu-worker -- sin el .env de la raíz (o sin esa variable en
él), sigue haciendo falta el flag --profile explícito. Sirve para
cualquier perfil, no solo igpu.
--profile monitoring, activo o no por defecto)Antes de levantarlo, crea backend/.env a partir de backend/.env.example
(ver variables de entorno más abajo).
Análisis de imagen en Docker (geolocalización + contenido visual): el
docker-compose.yml trae WITH_GEOLOCATION=true por defecto, que instala
torch/faiss/transformers (para DINOv2, geolocalización) y
llama-cpp-python compilado con soporte CUDA (para Moondream2, análisis
de contenido -- ver el extra vision de pyproject.toml y la etapa cuda-builder
del Dockerfile) en la imagen del backend, varios cientos de MB extra.
Pese al nombre del flag (heredado de cuando solo existía
geolocalización), esa misma build es la que necesita también el análisis
de contenido visual (app/vision/scene_analysis.py, Moondream2 vía GGUF)
-- no hay un flag independiente para activar solo una de las dos, aunque
desde el cambio a llama-cpp-python ya NO comparten las mismas
dependencias de Python (antes sí, ambas usaban transformers). Sin esto,
el backend puede tener el índice FAISS perfectamente construido y montado
y aun así reportar la geolocalización como "no disponible" -- esas
librerías hacen falta en el análisis, no solo para construir el índice.
Si no vas a usar ninguna de las dos funciones, ponlo a false para una
imagen más ligera. Los dos modelos (DINOv2 y Moondream2) se precargan en
el arranque del contenedor, no en el primer análisis (ver lifespan en
app/main.py). La caché de ambos se persiste en backend/data/hf_cache/
para no volver a descargarlos en cada reinicio; Moondream2 además cachea
ahí su versión cuantizada (ver MOONDREAM_QUANT_TYPE en
app/vision/scene_analysis.py) una vez generada, para no recuantizar en
cada reinicio.
Offload de DINOv2 a una iGPU (opcional, --profile igpu): en
máquinas con GPU dedicada + iGPU (Intel/AMD, vía DirectML sobre WSL2),
dinov2-igpu-worker descarga su propio torch==2.4.1+cu121 +
torch-directml -- deliberadamente en una imagen/venv completamente
aparte del backend, nunca instalado ahí (ver ADR-28 en
docs/src/09_architecture_decisions.adoc para el porqué: mezclarlo con
el torch/cu121 que ya usa el backend para DINOv2 corrompe ese
entorno). Actívalo con ENABLE_IGPU_OFFLOAD=true en backend/.env y
el perfil igpu al arrancar (a mano con --profile igpu, o por defecto
con COMPOSE_PROFILES=igpu en el .env de la raíz -- ver más arriba) --
ninguno de los dos por separado hace nada.
Construir backend + webapp + dinov2-igpu-worker los tres a la vez
(cada uno con su propia descarga de torch) puede saturar los recursos
de Docker Desktop/WSL2 y el build del worker termina con un
rpc error: code = Unavailable desc = error reading from server: EOF
justo al final (paso exporting layers) aunque los otros dos hayan
terminado bien. Si te pasa:
docker images | findstr amitraceable # confirma si backend/webapp sí terminaron
docker compose build dinov2-igpu-worker # reintenta solo el que falló
Si vuelve a fallar igual, es contención de recursos, no mala suerte puntual -- en orden de probabilidad:
wsl --shutdown y reabrir Docker Desktop (el demonio de BuildKit a
veces queda colgado tras un fallo así).%UserProfile%\.wslconfig:
[wsl2]
memory=8GB
y wsl --shutdown para aplicarlo.docker compose build backend
docker compose build webapp
docker compose --profile igpu build dinov2-igpu-worker
docker system df) -- los dos torch+cu121
(backend y worker, cada uno de varios cientos de MB/GB en wheels) más
las capas intermedias de build acumulan bastante; docker builder prune libera caché de builds antiguos sin tocar las imágenes en uso.backend/)cd backend
uv sync # crea .venv con las dependencias, el grupo dev y los modelos de spaCy
cp .env.example .env # y rellenar credenciales
uv run uvicorn app.main:app --reload --port 3000
(Requiere uv; descarga
solo el Python 3.12 que fija backend/pyproject.toml.)
Si quieres geolocalización por imagen o análisis de contenido visual (Moondream2) funcionando en este venv, hace falta un paso más -- lo de arriba, por sí solo, deja ambas funciones en "no disponible" de forma silenciosa (ver más abajo, "Alcance y limitaciones"): no falla con un error, simplemente esa parte del pipeline no aporta nada, así que es fácil no darse cuenta de que falta este paso.
uv sync --extra vision
Con Docker esto ya viene resuelto por defecto (ver WITH_GEOLOCATION más
abajo) -- este paso solo es necesario si ejecutas el backend directamente
con uvicorn, como aquí arriba.
En arranques posteriores, con el venv ya creado, basta con activar el entorno y lanzar uvicorn directamente.
cd webapp
npm install
npm run dev
La webapp estará en http://localhost:5173
Ver backend/.env.example para la lista completa comentada. Resumen:
| Variable | Obligatoria | Notas |
|---|---|---|
REDDIT_CLIENT_ID, REDDIT_CLIENT_SECRET, REDDIT_REDIRECT_URI, REDDIT_USER_AGENT | Sí | App tipo "web app" en https://www.reddit.com/prefs/apps |
SESSION_SECRET_KEY | Sí | Cadena aleatoria larga, firma la cookie de sesión |
INSTAGRAM_APP_ID, INSTAGRAM_APP_SECRET | No | Sin ellas, Instagram devuelve 503 pero Reddit sigue funcionando. App "API setup with Instagram Login" en Meta for Developers |
INSTAGRAM_REDIRECT_URI | No | Déjala vacía si usas Docker (ver nota de túnel más abajo): se deriva sola del Host de cada petición. Fíjala solo con dominio propio en producción |
FRONTEND_ORIGIN | No | Déjala vacía si usas Docker: se deriva sola del Host. Fíjala (p. ej. http://localhost:5173) solo si sirves frontend y backend en puertos distintos sin Docker |
ENABLE_AI_ANALYSIS | No | true por defecto. Desactiva del todo el análisis con IA (veredicto/conclusiones + extracción de atributos, ver backend/app/nlp/ai_client.py) sin desinstalar nada |
QWEN_GGUF_REPO_ID, QWEN_GGUF_FILENAME | No | Repo/fichero de Hugging Face con el GGUF de Qwen3.5-4B a cuantizar localmente (Q4_K_M vía llama-cpp-python) -- SIN VALOR POR DEFECTO A PROPÓSITO, hay que confirmarlo contra lo disponible en Hugging Face antes de desplegar (ver .env.example). Sin rellenar, la sección "Analizar con IA" del dashboard indica que no está disponible, sin afectar al resto |
ENABLE_SCENE_ANALYSIS | No | false por defecto. Activa el análisis de contenido visual con Moondream2 (descripción de escena por foto). Afecta exclusivamente a esto: la geolocalización por similitud visual (DINOv2) no lee esta variable y sigue funcionando igual, esté esto activado o no -- probado explícitamente en test_geolocation.py. Para que Moondream2 pueda funcionar (con esta variable en true) hace falta además WITH_GEOLOCATION=true en el build de Docker (mismo build-arg, aunque desde ADR-43 instala llama-cpp-python para Moondream2, no las mismas librerías que DINOv2). Ver ADR-43 en docs/ (sustituye a ADR-19, que documentaba el backend transformers anterior): la cifra de RAM/VRAM necesaria depende ahora de si MOONDREAM_QUANT_TYPE está cuantizando el modelo o no, no hay un único número fijo como antes |
Nota sobre Instagram y HTTPS en local: la API de Instagram (Business
Login) exige que redirect_uri sea HTTPS, incluso en desarrollo. Para
probarlo en local sin dominio propio, usa ngrok apuntando a la
webapp (puerto 8080), no al backend directamente -- la webapp lleva
su propio nginx que hace de proxy hacia el backend bajo el mismo origen
(ver webapp/nginx.conf), así que un único túnel sirve para todo:
ngrok http 8080
(requiere cuenta gratuita en https://ngrok.com y ngrok config add-authtoken <token> una vez).
Restringir quién puede entrar (Basic Auth): el plan gratuito de ngrok
incluye un dominio propio estable (no cambia en cada reinicio) y
permite añadir autenticación sin plan de pago, vía Traffic Policy.
Copia backend/policy.example.yaml a backend/policy.yaml (este
segundo nombre está en .gitignore, nunca se sube) y pon tu
usuario/contraseña real dentro:
on_http_request:
- actions:
- type: basic-auth
config:
realm: "AmITraceable"
credentials:
- "usuario:contraseña-larga-y-unica"
Y arranca el túnel apuntando a ese archivo:
ngrok http 8080 --url=tu-dominio.ngrok-free.app --traffic-policy-file=backend/policy.yaml
Así, quien no tenga esas credenciales ni siquiera llega al backend -- ngrok rechaza la petición en su propio borde, antes de reenviarla. Ver ADR-39 sobre por qué esto (y no dominio propio + Cloudflare) es la solución elegida.
⚠️ No usar cloudflared tunnel --url ... (Cloudflare Quick Tunnel):
el edge de trycloudflare.com bufferiza por completo las respuestas
text/event-stream, así que el streaming SSE de /api/analyze/{platform}/stream
(la pantalla de carga con el listado de fases) nunca llega en tiempo real
al navegador -- todos los eventos se reciben de golpe justo cuando el
análisis termina, y se ve solo "Analizando tu actividad pública en
X..." sin el listado de fases mientras carga. Es un problema conocido y
documentado del propio Cloudflare (no del backend: las cabeceras
text/event-stream y X-Accel-Buffering: no ya se mandan bien). Un
named tunnel de Cloudflare (con dominio propio) sí respetaría el
streaming, pero para desarrollo local sin dominio, ngrok funciona
correctamente sin ese problema.
Con INSTAGRAM_REDIRECT_URI y FRONTEND_ORIGIN vacías en .env (valor
por defecto), lo único que hay que hacer con la URL https://xxx.ngrok-free.app
que te dé cada vez es darla de alta en la app de Meta Developers (con el
path /auth/instagram/callback) -- eso sigue siendo manual, Meta no tiene
API para gestionar esa lista. Todo lo demás (.env, reconstruir imágenes,
reiniciar uvicorn) se adapta solo en cada reinicio del túnel.
La primera vez que abras la URL de ngrok en el navegador, el plan gratuito muestra una página intermedia de aviso ("You are about to visit...") -- solo hay que darle a "Visit Site", no es un error.
El módulo de geolocalización de fotos (app/vision/geolocation.py) es
opcional: sin el índice FAISS construido, simplemente no aporta nada al
informe, sin errores. Para activarlo:
cd backend
uv sync --extra vision
uv run python scripts/geolocalization/download_osv5m_spain.py --output data/osv5m_spain --max-disk-gb 35
uv run python scripts/geolocalization/build_faiss_index.py --images data/osv5m_spain
(El extra vision apunta al índice de PyTorch con CUDA 12.1
(pytorch-cu121, ver [tool.uv.sources] en backend/pyproject.toml) -- ver
también el ARG WITH_GEOLOCATION del Dockerfile si vas a correr esto
dentro de Docker.)
El análisis de contenido visual (app/vision/scene_analysis.py,
Moondream2) usa el mismo el extra vision de pyproject.toml, pero no necesita nada
de lo anterior: no hay índice que construir ni dataset que descargar, se
ejecuta directamente sobre cada foto.
download_osv5m_spain.py descarga solo las imágenes de España del
dataset OpenStreetView-5M
(streaming shard a shard, con límite de disco configurable, reanudable
tras interrupción).download_osv5m_world.py es la variante sin filtro de país (mucho más
pesada en tráfico de red, ~260GB).build_faiss_index.py extrae embeddings con DINOv2 y construye el
índice de búsqueda por similitud.recover_metadata.py reconstruye metadata.csv a partir de las
imágenes ya descargadas, sin volver a bajarlas -- pensado para el caso
(ya corregido, pero por si tienes un metadata.csv de una ejecución
anterior al fix) en que se perdía la columna id al guardar, dejando el
índice sin poder casar ninguna imagen con su fila de metadatos.Estos datos/artefactos no se versionan en el repositorio (ver
.gitignore): son regenerables ejecutando los scripts.
webapp/package.json)npm run dev — servidor de desarrollo.npm test — tests unitarios (Vitest, ~100 tests).npm run test:coverage — tests con cobertura (para Sonar).npm run test:e2e — tests E2E (levanta webapp + backend sueltos y corre Cucumber; ver webapp/E2E.md). Cubre la pantalla de consentimiento y que el dashboard exige autenticación real contra el backend (sin mockear), pero no el flujo completo de OAuth (necesitaría credenciales reales) ni el montaje con Docker/nginx.npm run start:all — levanta webapp + backend Python a la vez (conveniencia para desarrollo/E2E).npm run lint — ESLint.uv run uvicorn app.main:app --reload --port 3000 — arranca el backend en desarrollo.uv run pytest — tests unitarios (~260 tests, 1 se salta si no tienes instalado el extra vision en local).uv run pytest --cov=app --cov-report=xml --cov-report=term — tests con cobertura (genera coverage.xml para Sonar).El repo tiene un grafo generado con Graphify
(graphify-out/) que Claude Code consulta automáticamente en vez de releer archivos
sueltos, ahorrando tokens. Solo funciona así de forma automática dentro de Claude Code
(usa hooks internos de esa herramienta) — en un chat normal de Claude no hay ahorro
automático salvo que se le pida explícitamente consultar el grafo por terminal.
Al clonar en un equipo nuevo, ejecutar una vez (regeneran configuración local, no se versiona):
uv tool install graphifyy # o: pipx install graphifyy — evitar "pip" a secas
graphify claude install --project # regenera .claude/settings.json con la ruta de este PC
graphify hook install # auto-actualiza el grafo tras cada commit
graphify-out/ (grafo, informe, visualización) sí se versiona; .claude/settings.json,
graphify-out/cache/, graphify-out/cost.json y las carpetas de backup con fecha
(graphify-out/20YY-MM-DD/) no, porque son específicos de cada máquina — ver .gitignore.
Debido a restricciones de la cuenta gratuita, el token de Sonar debe
renovarse cada dos meses. Configuración en sonar-project.properties
(rutas de fuentes/tests, exclusiones, reportes de cobertura).
No incluido en el código, pero necesario antes de la defensa:
_WEIGHTS en
privacy_score.py) y de la asunción de independencia del estrechamiento
de población (k_anonymity.py) contra percepción subjetiva de usuarios
reales o correlaciones reales del Censo/EPA.