GitHub SBOM Asíncrono: Audita Dependencias sin Timeouts
TL;DR: GitHub ha migrado los exports de SBOM (Software Bill of Materials) a un flujo asíncrono con dos nuevos endpoints: generate-report y fetch-report. Desaparece el timeout de 10 segundos que bloqueaba auditorías en repos grandes, y cada export se procesa en background con un UUID. Aquí vas a ver cómo integrarlo desde Python y desde GitHub Actions sin romperte los dedos.
El problema: 10 segundos no bastaban para repos grandes
\n\nSi alguna vez has intentado exportar el SBOM de un monorepo con cientos de dependencias transitivas, conoces el final: pantalla en blanco, request caído y ningún archivo descargado. Hasta abril de 2026, el endpoint /repos/{owner}/{repo}/dependency-graph/sbom tenía un timeout fijo de 10 segundos. Para un proyecto pequeño funcionaba, pero en repos con grafos de dependencias complejos se rompía.
Peor todavía: cada reintento lanzaba un worker nuevo en el backend sin coordinación, así que acababas quemando recursos para nada. En escenarios reales de auditoría automatizada, esta limitación forzaba soluciones creativas: generar SBOMs con syft en local, subirlos a un bucket y escanearlos aparte. Funciona, pero es fricción pura.
El changelog del 14/04/2026 arregla esto desde la raíz: el export es asíncrono, el job se identifica con un UUID y el cliente hace polling hasta que el SBOM está listo.
\n\n¿Qué es un SBOM y por qué importa?
\n\nUn SBOM (Software Bill of Materials) es un inventario estructurado de todas las dependencias, versiones y metadatos que componen un proyecto de software. GitHub genera SBOMs en formato SPDX 2.3 a partir del grafo de dependencias del repo.
\n\n¿Por qué importa? Porque cuando se publica una CVE crítica en una librería transitiva (piensa en log4shell), un SBOM actualizado te permite responder en minutos a la pregunta ¿estamos afectados?. Sin él, toca inspeccionar a mano package-lock.json, go.sum, pom.xml y demás.
Qué ha cambiado exactamente
\n\n| Aspecto | Antes (síncrono) | Ahora (asíncrono) |
|---|---|---|
| Timeout | 10 segundos fijos | Sin timeout: polling hasta completar |
| Endpoints | /sbom único | /sbom/generate-report + /sbom/fetch-report/{uuid} |
| Reintentos | Workers duplicados en backend | Un único job por UUID |
| UI | Botón que bloqueaba | Página con polling y descarga diferida |
| Anónimos | Sin límite claro | Un request concurrente por repo |
Los dos endpoints nuevos
\n\nEl flujo asíncrono se basa en dos llamadas. La primera encola el job, la segunda lo recupera cuando está listo.
\n\n- \n
- Generar el reporte:
GET /repos/{owner}/{repo}/dependency-graph/sbom/generate-report. Devuelve unsbom_uuidúnico que identifica tu job. \n - Descargar el reporte:
GET /repos/{owner}/{repo}/dependency-graph/sbom/fetch-report/{sbom-uuid}. Mientras el job está en proceso devuelve201. Cuando termina, devuelve un302redirigiendo al SBOM final. \n
Un detalle importante: el SBOM refleja el estado del repo en el momento en que llamas a generate-report, no cuando descargas. Y solo se genera para el HEAD de la rama por defecto, no para ramas arbitrarias.
Implementación paso a paso en Python
\n\nVamos a construir un cliente que encola un SBOM y hace polling hasta que esté listo. Uso httpx por su soporte nativo de redirecciones controlables.
# Cliente asíncrono para el nuevo SBOM API de GitHub\nimport os\nimport time\nimport httpx\n\nGH_TOKEN = os.environ[\"GITHUB_TOKEN\"]\nREPO = \"owner/repo\"\nBASE = f\"https://api.github.com/repos/{REPO}/dependency-graph/sbom\"\nHEADERS = {\n \"Authorization\": f\"Bearer {GH_TOKEN}\",\n \"Accept\": \"application/vnd.github+json\",\n}\n\ndef request_sbom() -> str:\n # Encola el job y devuelve el UUID del reporte\n r = httpx.get(f\"{BASE}/generate-report\", headers=HEADERS, timeout=30)\n r.raise_for_status()\n return r.json()[\"sbom_uuid\"]\n\ndef fetch_sbom(uuid: str, max_wait: int = 120) -> dict:\n # Polling hasta que el SBOM esté listo o se agote el tiempo\n url = f\"{BASE}/fetch-report/{uuid}\"\n deadline = time.time() + max_wait\n while time.time() < deadline:\n r = httpx.get(url, headers=HEADERS, follow_redirects=True, timeout=30)\n if r.status_code == 200:\n return r.json()\n time.sleep(5)\n raise TimeoutError(f\"SBOM {uuid} no listo tras {max_wait}s\")\n\nif __name__ == \"__main__\":\n uuid = request_sbom()\n sbom = fetch_sbom(uuid)\n print(f\"Paquetes en el SBOM: {len(sbom.get('packages', []))}\")\n\n\nPuntos clave del código:
\n\n- \n
follow_redirects=Truetransparenta el paso de302al contenido real sin tener que manejarlo a mano. \n - El
time.sleep(5)es el intervalo de polling. No bajes de 2-3s o te arriesgas a rate limiting. \n max_waitde 120s cubre repos medianos. Para monorepos puedes subir a 300s. \n
Integración con GitHub Actions
\n\nEl caso de uso más directo: generar un SBOM en cada push a main y escanearlo contra una base de CVEs. Este workflow lo hace usando grype de Anchore para el escaneo:
# .github/workflows/sbom-audit.yml\nname: SBOM Audit\non:\n push:\n branches: [main]\n schedule:\n - cron: \"0 3 * * 1\" # Lunes 03:00 UTC\n\njobs:\n audit:\n runs-on: ubuntu-latest\n permissions:\n contents: read\n steps:\n - name: Generar y descargar SBOM\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n run: |\n UUID=$(curl -sH \"Authorization: Bearer $GH_TOKEN\" \\\n \"https://api.github.com/repos/${{ github.repository }}/dependency-graph/sbom/generate-report\" \\\n | jq -r .sbom_uuid)\n for i in {1..30}; do\n STATUS=$(curl -sL -o sbom.json -w \"%{http_code}\" \\\n -H \"Authorization: Bearer $GH_TOKEN\" \\\n \"https://api.github.com/repos/${{ github.repository }}/dependency-graph/sbom/fetch-report/$UUID\")\n [ \"$STATUS\" = \"200\" ] && break\n sleep 10\n done\n - name: Escanear con grype\n uses: anchore/scan-action@v4\n with:\n sbom: sbom.json\n fail-build: true\n severity-cutoff: high\n\n\nTres detalles del workflow que se pasan por alto fácilmente:
\n\n- \n
- El
cronsemanal es clave: aunque no haya cambios, las CVEs nuevas se publican a diario. Un scan solo en push no las detecta. \n severity-cutoff: highevita que el build falle por vulnerabilidades informativas. \n - Usa
secrets.GITHUB_TOKEN, no un PAT. El token efímero tiene permisos acotados al workflow. \n
En Producción
\n\nEl tutorial funciona contra repos pequeños, pero hay cosas que cambian cuando escalas. Si estás integrando auditorías automáticas en varios repos (o eres un SaaS que audita código ajeno), ten en cuenta esto:
\n\n- \n
- Rate limits y concurrencia: usuarios anónimos están limitados a un request concurrente por repo. Los autenticados no, pero el rate limit general de la REST API sigue vigente: 5.000/h para PATs, 15.000/h para GitHub Apps. \n
- Backoff exponencial: si integras esto en un agente que audita 50+ repos, añade backoff y cachea el UUID por commit. Reintentar
generate-reportpara el mismo HEAD genera otro UUID, pero el SBOM subyacente es idéntico. \n - Coste: la API es gratuita para repos públicos. En repos privados cuenta contra tu cuota de GitHub y no hay cargo extra por el procesamiento async. Si montas esto sobre un agente de IA, el coste real está en los tokens del LLM que analiza el SBOM: mira cómo reducir el gasto de tokens en agentes de código antes de lanzarlo contra 100 repos. \n
- Tiempo de generación: un repo Python con ~200 dependencias tarda 3-8 segundos en pruebas. Un monorepo JS con 5.000+ nodos puede irse a 30-60s. Ajusta tu
max_wait. \n - Freshness: el SBOM se calcula en tiempo real sobre el grafo de dependencias, que GitHub actualiza tras cada push. No cachees el resultado más de 24h si quieres detectar CVEs recientes. \n
Para flujos más avanzados donde un agente de IA analiza el SBOM y prioriza parches, este patrón async encaja con lo que vimos en Dependabot y agentes IA para el parche automático. El SBOM da el estado del inventario; el agente decide qué actualizar primero.
\n\nErrores Comunes y Depuración
\n\n- \n
- Error: recibo
404al llamar agenerate-report. Causa: el repo no tiene el grafo de dependencias habilitado (es opcional en repos privados). Solución: actívalo en Settings → Security → Dependency graph. \n - Error: el polling devuelve
201indefinidamente. Causa: UUID caducado o job muerto en backend. Solución: llama de nuevo agenerate-reportpara obtener un UUID fresco y descarta el anterior. \n - Error:
403 Forbiddendesde GitHub Actions. Causa: faltacontents: readen lospermissionsdel job. Solución: añádelo al bloquepermissionsdel workflow. \n - Error: el SBOM descargado tiene 0 paquetes. Causa: el lenguaje del proyecto no tiene un parser soportado nativo por GitHub. Solución: complementa con un generador externo como syft para esos lenguajes. \n
Preguntas frecuentes
\n\n¿Puedo generar un SBOM de una rama distinta a main?
\nNo. La API solo soporta HEAD de la rama por defecto del repo. Si necesitas SBOMs por rama, genéralos localmente con herramientas como syft o trivy contra el checkout de esa rama y súbelos como artefacto del workflow.
¿El endpoint síncrono antiguo sigue funcionando?
\nSí, /repos/{owner}/{repo}/dependency-graph/sbom mantiene compatibilidad con el timeout de 10 segundos. GitHub no ha anunciado fecha de deprecación a 16/04/2026, pero para cualquier flujo nuevo usa los endpoints async.
¿Sirve el SBOM para cumplir con EU Cyber Resilience Act o SSDF?
\nEl formato SPDX 2.3 que genera GitHub es aceptado por la mayoría de frameworks regulatorios (CRA europeo, SSDF del NIST, EO 14028 en EE.UU.). Valídalo con tu equipo de compliance, pero como fuente base funciona.
\n\nQué te llevas
\n\nEl cambio es pequeño en apariencia (dos endpoints nuevos) pero abre la puerta a auditar repos grandes sin hacks. Hemos visto cómo llamar al flujo async desde Python, integrarlo en un workflow de GitHub Actions con escaneo de CVEs, y qué cambia cuando lo llevas a producción real.
\n\nEl patrón generate → poll → fetch es el mismo que usan otras APIs async de GitHub (búsquedas grandes, exports de audit log). Si te familiarizas con él aquí, lo reutilizas en todas partes. Y si estás montando un agente de seguridad que monitoriza múltiples repos en el ecosistema de Agent HQ de GitHub, este SBOM es la pieza base sobre la que construir: el inventario vivo que el agente consulta antes de cada decisión. Si además quieres que ese agente escriba sus propias rutinas de auditoría reutilizables, échale un ojo a AgentHandover y skills automáticas.
\n\n¿Ya tienes un pipeline de auditoría de dependencias en tu repo? Cuéntamelo en Twitter @sergiomarquezp_, me interesa ver qué herramientas de escaneo estáis usando. En el próximo post engancharé este SBOM a un agente Claude Code que prioriza parches por score CVSS.
", "contentShort": "Los exports de SBOM en GitHub ya no sufren el timeout de 10 segundos. Ahora son asíncronos con dos endpoints nuevos (generate-report + fetch-report) que permiten auditar repos gigantes sin romperse. Te cuento cómo integrarlo con Python y GitHub Actions paso a paso.", "contentResume": "SBOM en GitHub ahora es asíncrono: aprende a usar los nuevos endpoints de la API para auditar dependencias en repos grandes sin timeout y con polling.", "urlSlug": "github-sbom-asincrono-auditar-dependencias", "tags": ["github-sbom", "supply-chain-security", "github-actions", "dependency-graph", "automatizacion-seguridad", "sbom-async"], "seoTitle": "GitHub SBOM Asíncrono: Audita Dependencias sin Timeouts", "metaDescription": "GitHub ha migrado los exports de SBOM a un flujo asíncrono con dos endpoints. Aprende a auditar dependencias en repos grandes con Python y GitHub Actions.", "readTimeMinutes": 8, "suggestedAngleType": "demo_resultado", "faqQA": [ { "q": "¿Puedo generar un SBOM de una rama distinta a main?", "a": "No. La API asíncrona de GitHub solo soporta HEAD de la rama por defecto. Si necesitas SBOMs por rama, usa syft o trivy contra el checkout de esa rama y sube el resultado como artefacto del workflow." }, { "q": "¿El endpoint síncrono antiguo sigue funcionando?", "a": "Sí, /repos/{owner}/{repo}/dependency-graph/sbom mantiene compatibilidad con el timeout de 10 segundos. GitHub no ha anunciado deprecación a 16/04/2026, pero para flujos nuevos conviene usar los endpoints async." }, { "q": "¿Sirve el SBOM de GitHub para cumplir con EU Cyber Resilience Act o SSDF?", "a": "El formato SPDX 2.3 que genera GitHub es aceptado por la mayoría de frameworks regulatorios (CRA europeo, SSDF del NIST, EO 14028). Como fuente base funciona; valídalo con tu equipo de compliance para los detalles concretos." } ] }