CreaRack-SL

Eje de seguridad OSV.dev en deps-freshness

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:

  1. 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.
  2. Batch processing: POST a https://api.osv.dev/v1/querybatch en 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-...).
  3. Enriquecimiento paralelo: Cada aviso se consulta en GET /v1/vulns/{id} para extraer database_specific.severity (Cloudflare/GHSA).
    • ThreadPoolExecutor con 8 workers.
    • Severidad desconocida (PYSEC sin nota) → "unknown".
  4. Falla suave: Si OSV no responde (timeout, error), esa dependencia queda con vuln_count=0 y 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: True basta redeploy · False hay que cambiar el pin · None OSV 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.yml actualizado 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.sql aplicada.

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 pantallaCampoQué dice
Distintivo junto a «En uso»installed_sourceDe 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 ficharesolvedLa mayor versión que la horquilla admite, cuando no coincide con la que corre
«basta redeploy» / «cambiar pin»fix_within_specSi el arreglo del aviso cabe en la horquilla ya declarada o hay que tocar el manifiesto
Distintivo naranja Sin techounbounded_majorLa 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» + tooltipmanifestsEl 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 los fixed que 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, anthropic lo habían hecho sin que la página lo dijera) — más riesgo de que el próximo resolved traiga 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]]