Resumen ejecutivo
Desde el 11 de julio de 2026, el robot semanal deps-freshness consulta cada dependencia del proyecto contra la base pública de avisos de seguridad OSV.dev. Si la versión en uso tiene una vulnerabilidad publicada, se reporta su gravedad (Crítico / Alto / Moderado / Bajo) y esas filas salen primero en la tabla, ordenadas por severidad. Primera pasada verificada: de 119 dependencias, 12 con avisos (Django 6.0.6→6.0.7 tapa 3 GHSA, vitest 2.1.9 crítico, cryptography/aiohttp/requests con warnings).
Motivación
La herramienta deps-freshness ya reportaba desfase semver (major/minor/patch) — útil para mantener librerías actualizadas. Pero no advertía sobre vulnerabilidades publicadas conocidas. La segunda fase del runbook previsto era integrar OSV.dev (fuente neutral, sin API key, cobertura PyPI/npm), enriquecer el snapshot con severidad de cada aviso, y priorizarlos en la UI.
Cambios en scripts/deps_freshness.py
Nueva función resolve_vulns(deps, workers=8)
Propósito: Cruza cada dependencia con OSV.dev para obtener avisos de seguridad.
Algoritmo:
- Filtrado: Solo consulta deps con versión resoluble. Desde el 28-08-2026 esa versión es
installed or resolved— la instalada (PROD → runner → lockfile) o la mayor que la horquilla admite; nunca el mínimo del spec (ver «Actualización 28-08-2026» más abajo). Evita ruido de OSV. - Batch processing: POST a
https://api.osv.dev/v1/querybatchen lotes de 100 (optimización de latencia).- Payload por dep:
{package: {name, ecosystem: "PyPI"|"npm"}, version}. - Respuesta: lista de avisos OSV (ej.
GHSA-abc-..., PYSEC-2026-...).
- Payload por dep:
- Enriquecimiento paralelo: Cada aviso se consulta en
GET /v1/vulns/{id}para extraerdatabase_specific.severity(Cloudflare/GHSA).- ThreadPoolExecutor con 8 workers.
- Severidad desconocida (PYSEC sin nota) →
"unknown".
- Falla suave: Si OSV no responde (timeout, error), esa dependencia queda con
vuln_count=0y la tabla sigue completa.
Campos que rellena en Dep (la dataclass vive desde el 28-08-2026 en scripts/deps_sources.py):
vuln_count: int— Número de avisos conocidos.vuln_ids: str | None— IDs separados por coma (GHSA-…, PYSEC-…).vuln_severity: str | None— Peor severidad:critical | high | moderate | low | unknown.fix_within_spec: bool | None— Desde el 28-08-2026:Truebasta redeploy ·Falsehay que cambiar el pin ·NoneOSV no publica versión con fix.
Orden global y tabla
Orden de filas (en collect()):
1. Con aviso de seguridad (vuln_count > 0), más grave primero (SEV_ORDER).
2. Sin aviso: ordenar por desfase semver (LAG_ORDER: major, minor, patch, unknown, uptodate).
3. Desempate: repo, nombre de paquete alfabético.
Tabla printed (print_table()):
Nueva columna: SEGURIDAD
- Formato: "N (SEVERIDAD)" si hay aviso, "—" en caso contrario.
- Ejemplo: "1 (critical)" para vitest, "3 (high)" para Django.
Cobertura de manifiestos ampliada
Antes: requirements.txt, frontend/package.json, terminal/agent/requirements-agent.lock.
Ahora: Se añaden tests/requirements.txt y tests/parity_validation/requirements.txt (marcadas is_dev=True).
- No se instalan en el job (solo pineadas), pero entran en el colector y el snapshot.
- Los paquetes nuevos en cualquier manifiesto cubierto se detectan automáticamente en la siguiente pasada.
.github/workflows/deps-freshness.ymlactualizado con los dos paths en el trigger de push.
Integración con Workspace
La herramienta web (/tools/deps en el Workspace — en su propio repo) rediseña la UI en paralelo:
- Nombres de columnas claros: Pedida / En uso / Última / Atraso / Seguridad.
- Badges con gravedad: Crítico (rojo), Alto (naranja), Moderado (amarillo), Bajo (azul).
- Enlaces directos: Cada badge enlaza a la ficha oficial del aviso en OSV.
- Desplegable “¿Cómo leo esta tabla?”: Explicación plain-English para no programadores.
- Migración D1: tabla
0048_deps_freshness_vulns.sqlaplicada.
Lo que la UI muestra desde el 28-08-2026 (migración 0052_deps_freshness_motor_fiable.sql, componentes DepsBadges.tsx / DepInfoModal.tsx / depsShared.ts):
| En pantalla | Campo | Qué dice |
|---|---|---|
| Distintivo junto a «En uso» | installed_source | De dónde se leyó la versión: PROD (contenedor de producción) · CI (lo que resolvió el runner) · lock (fichero de bloqueo) |
| «Se instalaría X» en la ficha | resolved | La mayor versión que la horquilla admite, cuando no coincide con la que corre |
| «basta redeploy» / «cambiar pin» | fix_within_spec | Si el arreglo del aviso cabe en la horquilla ya declarada o hay que tocar el manifiesto |
| Distintivo naranja Sin techo | unbounded_major | La horquilla no pone límite y ya se ha ido a otro major sola. En gris y sin naranja (unbounded a secas) si solo falta el techo |
| «en N manifiestos» + tooltip | manifests | El paquete aparece en varios manifiestos; la fila muestra la horquilla más restrictiva |
Y una tarjeta nueva en la cabecera: «Sin techo, major nuevo», que cuenta las horquillas descontroladas.
Datos de referencia (dry-run 2026-07-11)
- Total dependencias: 119.
- Con avisos de seguridad: 12.
- Ejemplos:
- Django 6.0.6 → 6.0.7: tapa 3 GHSA.
- vitest 2.1.9: 1 crítico.
- cryptography, aiohttp, requests: avisos moderados.
Actualización 28-08-2026 (task #275): las horquillas ya no mienten
El cruce contra OSV describía arriba evaluaba cada dependencia por su versión installed/pinned mínima de la horquilla (requests>=2.31.0 → “2.31.0”), no por la que pip/npm instalan realmente (la mayor publicada que la satisface). Eso inventaba avisos: 22 HIGH de aiohttp, 6 de requests, 2 de pytest que no existían en ninguna instalación real, contrastado contra el pip freeze de PROD.
scripts/deps_specs.py (nuevo) resuelve ahora cada horquilla a la versión real (Dep.resolved) antes de consultar OSV — los tres casos bajaron a 0 avisos. De paso añade dos señales nuevas al eje de seguridad:
fix_within_spec: con losfixedque devuelve OSV, cada fila dice si el aviso se tapa con un simple redeploy (hay versión con fix dentro de la horquilla ya declarada) o si hace falta cambiar el pin a mano.unbounded/unbounded_major: horquillas sin techo superior que ya han saltado de major solas (openai,google-genai,anthropiclo habían hecho sin que la página lo dijera) — más riesgo de que el próximoresolvedtraiga una sorpresa.
La “instalada” que se cruza contra OSV también puede venir ahora de PROD real vía [[entity—api—endpoint—installed-packages]], en vez de solo lo que resuelve el runner de CI. Detalle completo: [[entity—ci—service—deps-freshness-collector]].
Notas operativas
- Sin API key: OSV.dev es un servicio público.
- Timeout HTTP: 20 segundos por petición (consistente con latencia de PyPI/npm).
- Falla suave: Si OSV cae o demora, el robot sigue con snapshot sin severidad (vuln_count=0 para esa dep).
- Regla 15: La fuente de avisos es real — consultada contra la base oficial de GHSA/PYSEC en cada run, y desde task #275 contra la versión que la horquilla realmente resuelve.
- No toca runtime: Es herramienta del equipo, sin bump de
APP_VERSION.
Véase también
- [[entity—ci—service—deps-freshness-collector]]
- [[entity—ci—workflow—deps-freshness]]
- [[entity—api—endpoint—installed-packages]]
- [[entity—migrations—table—deps-freshness]]
- [[entity—scripts—service—resolve-vulns]]
- [[concept—infra—supply-chain-security]]