Codex Security, el escáner de seguridad de OpenAI, es el más descargado de su categoría en npm: 785.567 descargas en los últimos 30 días, 27.360 solo el 11 de noviembre. El repositorio acumula 12.416 estrellas, 978 forks, 87 colaboradores, 1.989 commits y 55 versiones desde julio.
También incluye el arnés de evaluación más cuidadosamente especificado que he visto en este sector. Dos mil setecientos treinta y siete casos etiquetados, fijados a un commit concreto del repositorio de origen y verificados con SHA-256. Veintiuna métricas derivadas con la fórmula escrita una por una. Un esquema de muestreo representativo con pesos inversos de probabilidad. Una frontera de etiquetas documentada que mantiene la verdad de referencia fuera del directorio de trabajo del modelo.
Y en 17.785 líneas de documentación, más las 55 notas de versión, no hay ni un solo valor publicado de precisión, exhaustividad, F1, exactitud o MCC.
No es una figura retórica: lo comprobé con grep. El repositorio define strict_precision, strict_recall, strict_f2, strict_mcc, decided_coverage, unsafe_closure_rate, remaining_analyst_workload y catorce más, con denominadores exactos, y después no imprime nunca un número para ninguna. El único porcentaje de la documentación que describe el comportamiento de un escaneo es un umbral de interfaz: «at 80% of a single interactive scan’s limit, the dashboard offers a higher total».
Así que hice lo único que puede hacer un auditor ante un arnés sin resultados: descargué el conjunto de datos al que apunta, verifiqué el hash y analicé yo mismo las etiquetas. Después instalé la CLI y ejecuté lo que se puede ejecutar sin cuenta de OpenAI. La composición del banco de pruebas resulta ser más informativa que cualquier puntuación.
La herramienta, antes de la crítica
Hay una versión injusta de este artículo, así que primero los hechos. Codex Security es un producto real con superficie real, no un envoltorio de un prompt.
| Elemento | Valor (leído del repositorio y npm, 13/11/2026) |
|---|---|
| Paquete | @openai/codex-security 0.10.0, Apache-2.0, publicado el 12/11/2026 |
| Binarios | cs y codex-security, mismo punto de entrada |
| Entorno | Node ^22.13 / ^24 / ^26, más Python 3.10+ |
| Modelo por defecto | gpt-5.6-sol, reasoningEffort: xhigh |
| Autenticación | Inicio de sesión de ChatGPT, o OPENAI_API_KEY / CODEX_API_KEY en CI |
| Otros proveedores | Amazon Bedrock, OpenRouter, Fireworks AI, Cheaper Inference |
| Importación de escáneres | 16 valores de --provider en 18 guías, sin autenticación ni llamada al modelo |
| Tamaño del repositorio | 1.580 archivos: 819 TypeScript, 158 Python, 148 markdown |
El producto hace seis cosas: escanear un repositorio o un diff, validar hallazgos candidatos contra la evidencia del código, aplicar parches, verificar una corrección existente, redactar SECURITY.md y modelos de amenaza, e ingerir los informes de otros escáneres en un banco de trabajo local con deduplicación. Exporta SARIF, JSON y CSV, y publica en Linear, Jira y GitHub. El SDK de TypeScript expone el mismo motor.
La descripción en GitHub es «finding, validating, and fixing security vulnerabilities». El arnés de evaluación mide una de esas tres palabras.
Si eres nuevo en la familia Codex, la guía completa de Codex cubre el resto de la cadena de herramientas; este artículo trata solo de las afirmaciones de medición del producto de seguridad.
El arnés que nadie cita
El banco de pruebas es SastBench, de Rival Security, y OpenAI no se limita a citarlo: incluye un ejecutor completo en evals/triage-finding/sastbench/. La configuración es inusualmente disciplinada:
- Conjunto de datos fijado al commit
b8d95b77deRivalSecurity/sastbench, con el SHA-256 del archivo (57c76563…c9487) comprobado en la instalación, junto con el número de registros y de casos por clase. - 275 revisiones de repositorio hidratadas exactamente en los commits a los que se refieren las etiquetas.
- La verdad de referencia vive en los metadatos de la prueba, nunca en una variable del prompt; el checkout legible por el modelo es un directorio desechable al que se le deniega explícitamente el conjunto con etiquetas.
- Un mapeo de veredictos documentado que decide cuánto cuesta una respuesta
needs_reviewbajo cada métrica. - Una muestra representativa de 100 casos con semilla fija y pesos por estrato, para que las iteraciones se comparen con una línea base sobre los mismos identificadores.
Si alguien quisiera diseñar una evaluación creíble para un agente de seguridad, se parecería bastante a esto. La tabla de métricas es la parte que conviene leer despacio, porque demuestra que sus autores pensaron en los modos de fallo que favorecen artificialmente a un escáner:
| Grupo | Para qué sirve | Métrica de ejemplo |
|---|---|---|
| Strict | Cuenta todas las filas; abstenerse o fallar es incorrecto | strict_mcc — correlación de Matthews, «balances both classes in the imbalanced SastBench dataset» |
| Decided only | Solo cuentan confirmed / not_actionable | decided_coverage — «low coverage can make decided-only quality look artificially strong» |
| Workflow | El resultado operativo de una cola de triaje | unsafe_closure_rate — «fraction of genuine alerts the skill would incorrectly auto-close» |
| Reliability | Si la ejecución produjo una respuesta utilizable | execution_or_parse_error_rate |
Dos de esas definiciones — decided_coverage y unsafe_closure_rate — son justamente las que casi todos los bancos de pruebas comerciales omiten, porque son las que castigan a un agente por callarse o por cerrar vulnerabilidades reales. Que estén ahí indica que quien escribió este arnés no intentaba producir una cifra favorecedora.
Y después: nada. El README dice que hay que «run the full benchmark after reviewing sample results and approving a cost and runtime budget». No hay resultado de referencia, ni línea base, ni objetivo, ni umbral de regresión. La única afirmación sobre puntuaciones en todo el repositorio es la contraria, en las notas de calidad de la aplicación de ejemplo: «no detection score is asserted until actual scanner output has been reviewed».
Hay otra fecha que merece atención. SastBench se envió a arXiv el 6 de enero de 2026. El repositorio de Codex Security se creó el 13 de julio de 2026. El banco de pruebas existía seis meses antes que el producto, así que ningún resultado publicado de SastBench puede contener una puntuación de Codex Security.
Qué hay realmente en esos 2.737 casos
Descargué el commit fijado, confirmé que el hash del conjunto de datos coincide con el valor que comprueba el instalador, y analicé los 2.737 registros. Esta es la composición.
| Medición | Valor |
|---|---|
| Casos | 2.737 (299 true_positive, 2.438 false_positive = 89,1 % negativos) |
| Repositorios / commits | 151 / 275 pares distintos |
| Mayor repositorio único | LabRedesCefetRJ/WeGIA (PHP) — 803 casos, 29,3 % del total |
| Los 5 primeros repositorios | 44,0 % de todos los casos |
| Casos en un mismo commit | máximo 27, mediana 9 |
| Alertas brutas de Semgrep agrupadas | 22.234 |
| Alertas en la clase negativa | 21.270 (95,7 %) |
| Casos negativos que agrupan ≥10 alertas | 429 (17,6 %), el mayor con 232 alertas |
| Ubicaciones por caso | TP media 2,78, máx. 14 — FP media 8,67, máx. 214 |
| Ubicaciones en rutas de librerías de terceros | 6.459 de 21.961 (29,4 %) |
Casos con vulnerability_name vacío | 219 (8,0 %) |
| Etiquetas CWE distintas | 139, de las cuales 81 tienen menos de 5 casos |
Léanse las cinco primeras filas juntas. Un banco de 2.737 casos suena a amplitud. En la práctica son 151 repositorios, y una sola aplicación PHP aporta casi un tercio del conjunto. Si un modelo tiene un punto ciego sistemático en ese código, la cifra titular se mueve 29 puntos. Si lo tiene en Rust, el banco casi no tiene nada que decir: las extensiones de archivo en las ubicaciones etiquetadas están dominadas por JavaScript (5.580), HTML (5.438), PHP (3.011), Python (2.069) y Ruby (1.695).
Ahora la fila de librerías de terceros. Casi un treinta por ciento de todas las ubicaciones apunta a bower_components/, ckeditor, jquery, bootstrap, mocha o node_modules. En el commit de WeGIA que examiné, un caso negativo agrupa 88 alertas repartidas por páginas de ayuda y otro agrupa 55 alertas por los scripts de compilación de Bootstrap. Eso no son hallazgos de la aplicación.
Y los vacíos: 219 casos, el 8 % del banco, llevan vulnerability_name como cadena vacía. CWE-353 aparece en 83 casos sin nombre alguno, porque la entrada del catálogo nunca se resolvió. Un agente que recibe uno de esos casos obtiene una ubicación, un número de CWE y la descripción genérica de MITRE, no una descripción de la alerta.
La etiqueta es un proxy, no un juicio
Aquí está la parte que cambia lo que significaría una puntuación.
Los 299 positivos llevan metadata.source = cvefixes. Los 2.438 negativos llevan metadata.source = semgrep. Los 2.737 registros usan aggregation_type = cwe_per_commit.
El proceso, tal como lo documenta el propio SastBench, es: construir una base de datos de CVE a partir de NVD y PyDriller, hacer checkout del commit vulnerable, ejecutar Semgrep y agregar los hallazgos por CWE y commit. Un caso es positivo cuando su CWE coincide con un CVE registrado para ese commit. Es negativo cuando Semgrep disparó en ese commit con una CWE que la tabla de CVE no cubre.
El artículo lo dice en el resumen, sin matices: SastBench «combines real CVEs as true positives with filtered SAST tool findings as approximate false positives». El diseño de evaluación de Codex Security lo repite con más detalle: «SastBench’s false-positive class is produced by its benchmark methodology and is not a fully human-adjudicated corpus. Results therefore measure agreement with SastBench labels, not absolute vulnerability truth.»
Así que la clase negativa no es un conjunto de alertas verificadas como benignas. Es un conjunto de alertas en commits donde se registró un CVE de otra CWE. De ahí se siguen dos consecuencias, y ambas se pueden medir en los datos.
Primera: las clases se entrelazan en el mismo código. El 91,1 % de los 269 commits positivos también contiene al menos un caso negativo (245 de 269). A nivel de archivo, 64 casos negativos apuntan a un archivo que un caso positivo también señala en el mismo commit, y 39 de ellos tienen rangos de línea solapados. Ninguno comparte archivo y CWE, que es exactamente lo que predice la construcción cwe_per_commit.
Un par concreto, de DumbWareio/DumbDrop en el commit 854f69a0c3b8. server.js líneas 393-405, la función sendNotification que construye una cadena de comando de apprise a partir de un nombre de archivo, está etiquetada true_positive CWE-78. server.js líneas 377-392, calculateDirectorySize, que llama a path.join mientras recorre el directorio de subidas, está etiquetada false_positive CWE-22. Los dos registros son adyacentes en el mismo archivo y llevan etiquetas opuestas: no porque alguien dictaminara que una función es segura y la otra peligrosa, sino porque en ese commit existía un CVE con CWE-78 y no uno con CWE-22.
Segunda: aquí la precisión premia el instinto equivocado. El 95,7 % de las alertas brutas están en la clase negativa. El 17,6 % de los casos negativos agrupa 10 o más alertas, y uno agrupa 232. Un modelo que responda «not_actionable» siempre que la ruta contenga bower_components o mocha despejará una parte enorme de la clase negativa sin leer una línea de código, y su strict_precision se verá excelente.
Juntando ambos hechos, la lectura honesta de una hipotética puntuación alta es estrecha: demostraría que el agente sabe suprimir ruido de directorios de terceros y coincidir con un proxy basado en una tabla de CVE. No demostraría que distingue una vulnerabilidad real de una benigna adyacente, ni se trasladaría a tu repositorio, donde la suposición por defecto —que la mayoría de alertas son falsas— suele estar equivocada en la dirección contraria.
Y hay un corolario que importa más que el banco de pruebas. Si tu agente confirma correctamente una vulnerabilidad real que ningún CVE cubre —el caso normal del fallo que sí llegaste a producción—, este banco lo puntúa como falso positivo.
La aplicación de ejemplo: 10 vulnerabilidades en 789 líneas
El repositorio sí contiene algo que se puede ejecutar de principio a fin sin cuenta ni modelo: examples/invoice-desk.
Son 789 líneas de Node sin dependencias repartidas en siete módulos —server.mjs, invoices.mjs, imports.mjs, integrations.mjs, accounts.mjs, store.mjs, views.mjs— con diez vulnerabilidades plantadas y una clave de respuestas en qa/expected-findings.json. Las diez son deliberadamente variadas: exposición de un secreto a través de un informe de soporte, autorización de objetos rota al consultar facturas, colisión de clave de caché entre espacios de trabajo, aprobación obsoleta tras editar, validación que falla abierta en la importación, reutilización de credenciales en un destino distinto, política anidada mutable compartida, comprobación que valida un campo de metadatos y persiste otro, asignación masiva en la actualización de perfil e inyección de HTML almacenado en notas.
Ejecuté el arnés. node --test examples/invoice-desk/qa/app.test.mjs da 21 pruebas, 21 correctas, incluida una por escenario plantado y una que verifica que el manifiesto cubre los diez.
Dos cosas que notar. Una vulnerabilidad cada 78,9 líneas de código de aplicación es una densidad que no verás nunca, así que un escáner que acierte 10 de 10 aquí solo demuestra que encuentra fallos en un objetivo construido para ser encontrado, igual que un 100 % en un dictado no dice nada sobre la prosa. Y el propio README del directorio de calidad es cuidadoso con esto: «Treat unexpected findings as needing review rather than automatically declaring them false positives», y «no detection score is asserted until actual scanner output has been reviewed».
Esa segunda frase es el artículo entero en una línea: el fabricante incluyó una clave de respuestas y decidió no corregir contra ella.
Qué funciona sin cuenta (lo probé)
Instalé @openai/codex-security 0.10.0 globalmente y recorrí la superficie sin credenciales.
cs scan ./app --dry-run devuelve código 0 e imprime «Preflight complete», seguido de una configuración resuelta que incluye authentication.verified: false, model: gpt-5.6-sol y reasoningEffort: xhigh. Es un ensayo en seco que pasa sin certificar nada sobre tu capacidad de autenticarte.
Un cs scan ./app real falló después en esta máquina, antes de cualquier llamada al modelo:
Codex could not run a command in its sandbox: bwrap: No permissions to create a new namespace…
Ese error concreto es una propiedad de esta máquina —un contenedor sin privilegios y sin namespaces de usuario—, no un defecto del producto. La documentación cubre el caso de contenedor con un override de Compose y un perfil de AppArmor. Pero ilustra un punto operativo real, y la incidencia abierta #2575 lo formula explícitamente: --dry-run «check[s] local inputs without starting Codex or loading credentials», así que no puede demostrar que el sandbox ni la ruta de credenciales funcionarán en tu runner de CI. Quien abrió esa incidencia pedía una forma soportada y sin modelo de cualificar el sandbox real de los workers, y recibió una petición de documentación.
Lo que sí funcionó sin cuenta fue la ruta de importación, y funcionó limpiamente. Siguiendo el ejemplo del propio repositorio, con un repositorio git desechable cuyo remoto coincide con el campo repository del informe:
cs publish findings trivy-report.json --provider trivy --to local \
--dry-run --source-key example/project/main
devolvió código 0, "read": 1, "ready": 1, "excluded": [] y un hallazgo normalizado con identificador estable. Mis dos intentos anteriores fallaron con errores de validación precisos y legibles por máquina (Missing required option --source-key, luego Imported repository "https://github.com/example/project" does not match a configured remote), ambos con código 2 y con code y fieldErrors. Es buena higiene de CLI y merece decirse: la superficie gratuita de esta herramienta es la fontanería para los informes de otros dieciséis fabricantes, y esa fontanería está mejor construida que el preflight del escaneo.
El modo de fallo que te cuesta una versión
Nada de lo anterior importaría mucho si un escaneo limpio significara un repositorio limpio. La incidencia abierta #2415 —sigue abierta, reportada contra 0.8.3 y sin corregir en main en 9b3ce7a— dice que no.
Cuando un Deep Scan cruza --max-cost después de que la fase de descubrimiento haya terminado, los candidatos restantes nunca se validan, y entonces:
the bundle is sealed with
scan.status: "completed". The scan contract definescompletedas “The requested scan reached normal completion” and says “Onlycompletedsupports a completed-scan conclusion”. The SARIF export reportsexecutionSuccessful: true, the CLI emitsscan.completedand exits0.
La misma condición durante el descubrimiento produce lo contrario: scan.failed classification="cost_limit_exceeded" y código 2. Quien reportó el fallo siguió la ruta con precisión: budget_exhausted_draft registra los candidatos sin validar como needs_follow_up y añade una entrada diferida con id scan-cost-limit, luego complete_scan_locked sella el paquete con el estado por defecto completed, y finalize_scan_contract.py deriva execution_successful únicamente de ese estado. El motivo lo conoce el código —readDeepScanStop produce «Reached the … cost limit. More issues may remain.»— pero solo llega al resumen humano. El evento scan.completed no tiene campo de motivo de parada.
Los nombres de las pruebas del propio informador son el mejor resumen: test_budget_exhaustion_with_no_candidates_is_honestly_partial sella como complete un escaneo cortado por coste sin ningún hallazgo, y el único marcador es un identificador diferido no documentado. Y después, la frase que debería cerrar cualquier discusión sobre esta clase de error:
Candidates that were discovered but not validated are not SARIF results. Suppose a SARIF consumer closes alerts that are missing from a newer successful analysis. It can then close alerts this run never re-checked.
Un segundo informe abierto, #2446, es la misma forma por otra causa: los workers del Deep Scan agotaron un límite de uso, la ejecución finalizó en vez de pausarse, y el informe se produjo tras revisar por completo 17 de 2.100 archivos —un 0,81 % de cobertura—. En su descargo, el resumen del propio agente lo declaró en prosa. El informe existe; el estado se finalizó igualmente.
Es la misma forma que el hueco de sandbox de OpenMausBot: la función existe, pero la ruta por defecto no la activa. La diferencia es que aquí el síntoma no es una dependencia ausente, sino un campo de estado que dice «completado».
La documentación no deja margen para llamar sorpresa a esto. --max-time se describe como orientativo: «When selected, it is advisory: spend the available time on useful investigation… This is not a hard timeout». --max-cost «stops the scan and workers after estimated cost exceeds the limit; in-flight requests can finish above it», y «Post-scan prompts run after cost tracking and are outside this limit». El ejemplo de CI trae un límite de 5 dólares y reporta hallazgos sin hacer fallar el trabajo. Y alcanzar un límite «with a valid finalized report returns 0 by default».
La política de recompensas cierra el círculo. SECURITY.md afirma que «Missed findings, false positives, incorrect scan results, and performance issues are ordinary bugs», salvo que además permitan saltarse una restricción impuesta. Para un escáner de seguridad, los hallazgos omitidos son el modo de fallo principal del producto, y quedan explícitamente fuera de alcance.
Qué verificar antes de fiarte de un escaneo en verde
Es una buena herramienta con un hueco de medición y un error de estado peligroso. Los dos se pueden tapar desde fuera en una tarde.
- Comprueba el registro de diferidos, no el estado. Analiza el informe o el manifiesto guardado y haz fallar el trabajo si
deferredcontiene una entrada conid: "scan-cost-limit", o si algún candidato lleva un motivoneeds_follow_upque empiece por «Validation was deferred because the scan reached its cost limit». Hoy es la única evidencia legible por máquina de que hubo un corte por coste. - Compara cobertura con inventario. Toma el número de archivos revisados del informe y compáralo con el total del repositorio. El caso del 0,81 % pasó todas las comprobaciones de estado disponibles.
- Demuestra el sandbox en el runner real. Ejecuta un escaneo verdadero en la imagen exacta de CI antes de conectarlo a una comprobación obligatoria, y verifica
authentication.verified: trueen un preflight cuya salida realmente analices.--dry-runpor sí solo no te lo dirá. - Obtén tu propia línea base con
examples/invoice-desk. Registra versión del escáner, modelo, modo y esfuerzo de razonamiento junto al resultado: el valor por defecto de 0.10.0 esxhigh, y las estimaciones de coste del README no sobreviven a un cambio de modelo. Compara con los diez hallazgos esperados y cuenta los inesperados por separado, no como falsos positivos. - Construye un conjunto de 20 casos etiquetados con tu propio historial de incidencias. Ejecuta un commit vulnerable y su corrección por cada caso, como hace el carril de calibración del repositorio. Esa cifra es la única tasa de detección que significa algo para tu código. Veinte casos tuyos valen más que 2.737 ajenos, y los puedes tener esta semana.
Preguntas frecuentes
¿Hace falta una cuenta de OpenAI? Sí para escanear. cs scan necesita inicio de sesión de ChatGPT o una clave API en el entorno. La importación y la exportación no: publish findings --to local --dry-run funciona sin conexión, sin sesión y sin llamada al modelo.
¿Es gratis? No. Los escaneos consumen uso del modelo, el ejemplo de CI fija un límite estimado de 5 dólares por escaneo y el esfuerzo de razonamiento por defecto es xhigh. La importación, la exportación y la deduplicación son locales.
¿Sustituye a Semgrep o Snyk? No como sustituto, y no lo intenta. Escanea y valida con un modelo y, por separado, ingiere los formatos de informe de otros dieciséis escáneres en un banco de trabajo local para deduplicar y triar entre ellos.
¿Qué mide realmente el banco de pruebas incluido? El triaje de hallazgos ya dados: le entregas una alerta con su CWE, su ubicación y su descripción, y responde si se confirma o no. Su propio documento de diseño lo dice: «It does not measure whether an LLM or deterministic scanner discovers vulnerabilities that were never provided as input.»
¿Por qué no publican ninguna puntuación? Ninguna fuente lo explica. Lo busqué en el README, en la documentación de evaluación, en el changelog y en las 55 notas de versión. No hay explicación. La ausencia solo llama la atención porque construyeron el aparato; no voy a inventar un motivo.
¿Está corregido el error del estado en los cortes por coste? No a fecha de 0.10.0 (12/11/2026). La incidencia #2415 está abierta, se reprodujo en 0.8.3 y se comprobó en main en 9b3ce7a, y quien la reportó afirma que la ruta de código no ha cambiado desde 0.8.2.
La cifra que deberías querer
Un escáner de seguridad es la única categoría de software donde «sin novedades» es el resultado más caro posible. Un falso positivo cuesta veinte minutos de revisión. Un falso negativo te cuesta una divulgación, y un truncamiento sin etiquetar te cuesta la capacidad de distinguir entre ambos: obtienes una marca verde, un SARIF con executionSuccessful: true y ninguna forma de saber que la ejecución se detuvo con un 0,81 % de cobertura.
Codex Security ya ha hecho la parte difícil. Construyó un arnés de 21 métricas que mide la abstención, el cierre inseguro y la carga del analista, lo fijó a un hash y mantuvo las etiquetas fuera del alcance del modelo. Eso es más rigor del que casi ningún fabricante de este sector llegará a publicar, y está disponible hoy: pnpm --dir evals/triage-finding run eval:sastbench con tu propio presupuesto.
Lo que no ha hecho es contarle el resultado a nadie, y la composición del conjunto sugiere por qué podría resultar incómodo. Un banco cuya clase negativa es un proxy de tabla de CVE, con el 29,4 % de su evidencia en librerías de terceros y el 29,3 % de sus casos procedentes de una sola aplicación PHP, producirá una cifra de precisión fácil de leer como afirmación de producto e imposible de leer como predicción sobre tu repositorio.
Así que la postura útil es la que recomienda el propio repositorio: ejecútalo, revisa la salida y no aceptes una cifra que no hayas medido. Incluida la mía.