Addendum R2: La tasa que faltaba, medida sobre las dos clases que faltaban
Addendum R2 — la tasa que faltaba, medida sobre las dos clases que faltaban Este artículo sustituye como lectura al anterior —«Addendum: corregí mi propio instrumento y el número bajó de 807 a 612»: https://dev.to/ale
Addendum R2 — la tasa que faltaba, medida sobre las dos clases que faltaban
Este artículo sustituye como lectura al anterior —«Addendum: corregí mi propio instrumento y el número bajó de
807 a 612»:https://dev.to/alexisromtz7/addendum-corregi-mi-propio-instrumento-y-el-numero-bajo-de-807-a-612-5172—
cuyo cuerpo se publicó sin el Markdown con que fue escrito: la plataforma guardó el texto y descartó la sintaxis.
El contenido es el mismo; la forma no. Se republica para que se pueda leer lo que se midió. El original sigue vivo y
sin editar, como registro de lo que el entorno de publicación hizo con él (§9).
Este documento no reescribe el anterior. El addendum publicado con RC 2 y RC 4
(ADDENDUM_ARTICULO_20261030.md, blob 723dda49, vivo en Dev.to) queda intacto byte a byte, porque un acto
público no se corrige por edición: se corrige por adenda. Lo que sigue sustituye solo su sección «La tasa que
faltaba», y solo en la parte que dejó abierta.
Huella de esta revisión: HUELLA-ADDENDUM-R2-TPR-RC1-RC5-20261030. Se declara como texto en código y no como
comentario HTML por la razón medida en §9.
1. Lo que quedaba abierto, dicho con la cifra al lado
El addendum anterior cerró la tasa de RC 2 y RC 4: 33 casos, 100% formal, 15.2% sustantiva. Y declaró su
propio residuo en la misma página: RC 1 (60) y RC 5 (308) seguían sin tasa.
Eso significa que 368 de las 612 observaciones publicadas — el 60,1% del numerador — no tenían veredicto. Un
cliente que preguntara «¿cuál es tu tasa de falsos positivos?» recibía una respuesta que valía para 33 casos y se
presentaba como si valiera para 612. Esa era la debilidad, y no estaba escondida: estaba escrita.
Ya no está abierta.
2. La medición
Misma disciplina que la vez anterior, y en el mismo orden: primero la predicción, después la lectura.
Se escribió un pre-registro (preregistro_tpr_rc1_rc5_20261030.json, 129 líneas, blob 05eaead8) con las clases
sustantivas, el sesgo declarado y cinco falsadores, antes de abrir un solo run: o un solo uses:. Después se
re-descargaron los 163 archivos de workflow del lote por blob SHA desde la API de GitHub y se verificó su
sha256 contra el manifiesto: 163/163 idénticos, 0 fallos. El motor reprodujo el reparto exacto:
RC 1 60 · RC 2 30 · RC 4 3 · RC 5 308.
| clase | lectura | resultado |
|---|---|---|
RC 5 REF_MUTABLE |
¿el uses: citado existe en el crudo y su ref es realmente flotante? |
308/308, 0 falsos positivos |
RC 1 AUTORIDAD_CONTAMINADA |
¿el job tiene escritura y ejecuta código en la misma línea citada? | 60/60, 0 falsos positivos |
Control negativo RC 5 |
de 1321 uses:, 1013 están fijados a SHA de 40: ¿cuántos marca el motor? |
0 de 1013 |
Control negativo RC 1 |
164 jobs ejecutan código de terceros sin scope de escritura: ¿cuántos marca? | 0 de 164 |
Los dos controles negativos son la parte que importa. Un motor que marcara todo habría dado 1321 en lugar de 308, y
174 en lugar de 60. No los da: discrimina en los dos sentidos de la conjunción.
3. La tasa sustantiva: aquí es donde el número se encoge
Formal 100% no es la cifra que se puede vender, y el addendum anterior ya lo dijo para RC 2. Para RC 1 y RC 5
el efecto es mayor, porque estas dos clases se miden por ocurrencia y no por riesgo.
RC 5 — 308 ocurrencias no son 308 riesgos: son 31
El barrido de RC 5 no deduplica. Una misma acción repetida en veinte jobs produce veinte hallazgos. En los 163
archivos hay 1321 valores uses:, de los cuales 308 son flotantes… y esos 308 son solo 31 referencias
distintas. Publicar 308 sin decirlo sería inflar el numerador por la puerta de atrás.
| clase | ocurrencias | distintas | qué es |
|---|---|---|---|
Primera parte (actions/*) |
239 | 17 | etiqueta móvil de GitHub sobre código de GitHub. Patrón documentado |
| Tercero, etiqueta flotante | 42 | 5 |
pnpm/action-setup@v6, dtolnay/rust-toolchain@stable, oven-sh/setup-bun@v2, rust-lang/crates-io-auth-action@v1, vercel/next.js/.github/actions/pr-auto-label@canary
|
| Workflow reutilizable flotante | 10 | 6 | un workflow entero de otro repo ejecutado con el token de este |
| Placeholder sin sustituir | 17 | 3 | ver §4 — clase que la predicción no tenía |
| Imagen docker sin digest | 0 | 0 | predije 8; no hay una sola en los 10 repos |
La superficie de terceros real: 52 ocurrencias y 11 referencias distintas — el 16,9% de las ocurrencias y el
35,5% de las distintas. Las otras 256 se reparten en 239 actions/*, que para un cliente es ruido (el control del
ref lo tiene GitHub, no el atacante que el cliente teme), y 17 placeholders que no resuelven y que van contados como
clase propia en §4, no como terceros.
Corrección con cadena (
HUELLA-ADDENDUM-R2-CORRECCION-PORCENTAJE-TERCEROS-20261030). La primera versión de este
párrafo, escrita horas antes y nunca publicada, decía 22,4% y «las otras 239». Ese porcentaje es correcto para 69
ocurrencias, no para 52: la frase había sumado al numerador las 17 deR7, que son rutas$/.github/…del propio
repositorio del cliente y por tanto no son código de un tercero. La tabla de arriba nunca dijo 69; decía 42 + 10.
El número de la prosa y el de su propia tabla no medían lo mismo, y nada los comparaba. Se conserva el literal
equivocado a la vista porque un addendum que corrige cifras no puede editarse en silencio —esa es la misma regla que
lo hizo nacer—. La corrección viaja con el censo:
INVESTIGACION/evidencia/censo_tpr_rc1_rc5_20261030.json, camposuperficie_de_terceros.correccion_con_cadena.
RC 1 — 60 hallazgos, 10 sostienen el nombre del código
RC 1 se llama AUTORIDAD_CONTAMINADA. La conjunción que mide —escritura y ejecución en el mismo job— es
correcta y da 60/60 formal. Pero solo una parte de esas 60 líneas ejerce o entrega la autoridad:
| clase | n | qué es |
|---|---|---|
| A1 — escritura remota ejercida | 9 |
gh run cancel, gh pr comment, gh api --method POST: el token es el instrumento de esa misma línea |
| A2 — descarga a ejecución | 1 | `curl https://rustwasm.github.io/…/init.sh -sSf \ |
| A3 — hereda terceros | 28 | {% raw %}pnpm install, cargo build, npx -y [email protected], cargo binstall, gh corriendo con el token disponible |
| A4 — mutación local | 5 |
git config sin push: la autoridad está declarada y no se ejerce |
| A5 — código del propio repo | 11 |
pnpm run build, node scripts/…: el que ve el token es el dueño del repo |
| A7 — lexema sin ejecución | 6 | ver §4 |
Autoridad ejercida o entregada: 10 de 60 (16,7%). Ese es el número que la palabra «contaminada» puede defender.
4. Las dos clases que la predicción no tenía
En dos días consecutivos, el mismo patrón: la taxonomía se queda corta y la clase nueva aparece en el mismo lugar
donde el motor lee bien. Ayer fue NO_SUMIDERO (16 de 33). Hoy son dos, y las dos se declaran en vez de encajarse.
R7 — placeholder sin sustituir: 17 ocurrencias, 3 literales, 3 repositorios y 6 archivos. Los bytes crudos
contienen referencias locales escritas con el placeholder de plantilla de GitHub ($/…) que nunca se sustituyó:
$/.github/actions/setup 5 veces, todas en sveltejs/svelte ci.yml; $/.github/actions/issues-helper
11 veces en vitejs/vite —6 en bot.yml, 3 en issue-labeled.yml, 1 en issue-close-require.yml, 1 en
lock-closed-issues.yml—; y $/.github/workflows/notify-resolved-issues.yml 1 vez en nuxt/nuxt release.yml.
Los bytes son genuinos del origen: el git hash-object de los seis archivos coincide uno por uno con el blob_sha
que declara el manifiesto, así que no es un artefacto de la descarga. Su severidad queda NO_MEDIDO, por la misma
razón que NO_SUMIDERO: 0 si se lee que un uses que no resuelve no custodia nada, y «el hallazgo más fuerte» si se
lee que el archivo declara una intención que no ejecuta y quien lo audite creerá que está fijado. Resolverlo exige
saber si el repo genera ese archivo o lo commitea tal cual, y el archivo no lo dice.
Corrección con cadena (
HUELLA-ADDENDUM-R2-R7-POR-ARCHIVO-20261030). La versión anterior de este párrafo, escrita
horas antes y nunca publicada, nombrabafacebook/reactcomo uno de los repositorios con placeholders. Medido sobre
los 163 workflows ya verificados porsha256:facebook/reacttiene 26 workflows y cero ocurrencias, y los 17
están ensvelte(5),vite(11) ynuxt(1). El conteo nunca cambió —17 ocurrencias, 3 literales—; la atribución
sí. Es el tercer caso del mismo defecto en la jornada: un campo estructurado correcto al lado de una frase que no
se deriva de él, igual que el 22,4% de §5. La corrección viaja con el censo, campo
clase_no_prevista.correccion_con_cadena.
A7 — el lexema casa sin que nada se ejecute: 6 de 60. Un echo " - NPM name: …" casa con el patrón de npm.
Un yq '.allowBuilds[]=false' -i pnpm-workspace.yaml casa porque el nombre del archivo dice pnpm. No es un
defecto del motor —su dominio declarado es léxico por línea— pero sí es ruido para un cliente, y por eso se cuenta
aparte y se publica: es la forma en que un falso positivo sobrevive a la corrección del motor.
5. Cuatro defectos del lector, encontrados antes de publicar
La lección de la vez anterior se aplicó con saña: el lector también se equivoca, y cuando se equivoca declara
culpable al instrumento correcto. Un lector independiente de mesa, sin el parser del motor, encontró cuatro:
-
permissions: {}leído como{'{}': 'write'}→RC 1inflado de 60 a 121 (61 falsos rojos). - El
uses:de job contado dos veces →RC 5inflado de 308 a 319 (11 falsos rojos). - Valores de permiso entrecomillados (
contents: 'read') conservando las comillas → un job real quedaba fuera: 59 de 60. -
run:plegado en varias líneas leído solo en la primera → una línea citada aparecía truncada.
Los cuatro eran del lector. El motor no cambió una línea: su salida formal se sostiene 368/368.
6. Predicción contra medición, sin maquillar
| predicción (pre-registrada) | medido | veredicto |
|---|---|---|
RC 5: 308 TP, 0 FP, ±4 |
308 / 0 | cumplida |
RC 1: 60 TP, 0 FP, ±6 |
60 / 0 | cumplida |
| 31 a 60 literales distintos, punto 45 | 31 | cumplida al borde, por un literal |
Reparto RC 5: 240 / 55 / 8 / 3 / 2 / 0 |
239 / 42 / 0 / 10 / 0 / 17 | dirección correcta, tres desvíos |
| Terceros distintos: 12 ± 6 | 11 | cumplida |
A1+A2: 9 ± 6 |
10 | cumplida |
Tres desvíos que se publican porque se midieron: la imagen docker sin digest no existe en los 10 repos (predije
8), el workflow reutilizable flotante es el triple de lo predicho (3 → 10, y es la clase más fuerte de las
cuatro), y apareció una clase que no estaba en la taxonomía (R7, 17). El punto de 45 contra 31 no es un fallo de
método: es la diferencia entre estimar cuántas ocurrencias habría y contar cuántas referencias distintas hay.
7. Lo que sigue sin tasa, y por qué se dice
SC1 — 211 observaciones, y no todas son la misma cosa. No son del gate: son el recuento de sustitución y cadena
del propio arnés. Con este acto se miden 368 de 612 observaciones (60,1%) — pero ese denominador no es el de
hallazgos, y confundirlos es el defecto que este addendum ya cometió una vez con otra cifra. De las 244 sin censo,
156 son REVIEW por diseño del contrato (SC1_JOB_WRITE_SCOPE_REVIEW 153 + SC1_ROOT_READ_ALL_REVIEW 3): son
inventario, no hallazgo, y no hay tasa que asignarles porque no afirman una vulnerabilidad. 55 sí son conjunción
real (SC1_ROOT_PERMISSIONS_MISSING 36 + SC1_ROOT_WRITE_INHERITED 19). Y 33 son la clase corregida (RC 2 30
-
RC 43), validada por revisión de dominio con cadena, no por censo. Sobre el denominador que un CTO querría —el de las que afirman una vulnerabilidad, 456— la cobertura es 368/456 = 80,7%, y lo que falta censar son 55 casos, el 9,0% del total, no 211. Lo que se publica como 60,1% sigue siendo correcto mientras el denominador sea «observaciones»; deja de serlo en cuanto el denominador es «hallazgos».
Huella de esta revisión: HUELLA-ADDENDUM-R2-DENOMINADOR-20261030 (§MEDICIÓN CON UNIDAD, aplicado al propio
enunciado: el 60,1% no estaba mal, estaba midiendo otra población).
Y tres residuos más, declarados: RC 5 emite job: '-', así que no dice qué job consume la acción y la
severidad por job queda NO_MEDIDA; el residuo propio del motor sigue vigente (env: con secretos + ejecución de
terceros sólo cae bajo RC 1 si hay scopes de escritura); y la lectura sustantiva es de mesa, sobre el YAML, sin
ejecutar los workflows: no mide si una acción de terceros es hostil hoy, mide si el ref es fijable y si la autoridad
se ejerce.
8. La frase que sí se puede publicar
612 observaciones, de las cuales 456 afirman una vulnerabilidad. Censadas: 368 — el 80,7% de los hallazgos y el
60,1% de las observaciones. De las 244 restantes: 156 son inventarioREVIEWpor contrato, 55 son conjunción
real sin censar y 33 son la clase corregida, validada por revisión de dominio.
TPR formal 368/368. Control negativo: 1013 refs fijadas sin marcar y 164 jobs sin escritura sin marcar.
Sustantiva: 10 de 60 hallazgos de autoridad ejercen o entregan la credencial; 11 referencias distintas de
terceros en 308 ocurrencias de ref flotante; 17 referencias que no resuelven —5 ensveltejs/svelteci.yml,
11 en cuatro workflows devitejs/vite(6 de ellas enbot.yml), 1 ennuxt/nuxtrelease.yml—, con
severidadNO_MEDIDO.
No hay un número redondo y no lo va a haber. Lo que hay es un instrumento que se equivocó 195 veces, lo publicó, se
corrigió, midió su propia tasa, encontró cuatro defectos en su lector y los publicó también, y dejó a la vista lo
que no pudo medir. Eso es lo que un CTO puede auditar. Un «1000 vulnerabilidades» no.
9. Nota de publicación: el formato declarado y el formato ejecutado
El addendum anterior se escribió en Markdown y se publicó sin él. Medido hoy contra la API pública (GET, HTTP 200,
/api/articles/4950016published_at 2026-10-31T00:18:48Z): el body_markdown almacenado contiene
0 **, 0 comillas invertidas, 0 | y 0 ##. La pérdida no está en el render: está en la fuente.
Por eso el formato no se recupera cambiando de modo en el editor — hay que reescribir el cuerpo.
Y hay un detalle que sí es un mecanismo, no un accidente: de los dos comentarios HTML del cuerpo publicado, el que
abría línea perdió su < (!-- HUELLA-TPR-MEDIDA…, visible como texto) y el que iba en línea detrás del -->
sobrevivió intacto (<!-- HUELLA-NO-SUMIDERO-MEDIDO…). Regla falsable con n=1 cuerpo: Dev.to elimina el primer <
de una línea que empieza por <; los siguientes de la misma línea sobreviven. No es «el principio del cuerpo», como
se declaró en la primera lectura: el cuerpo empieza por texto, no por <. Una sola observación no es una regla: se
publica con su n y su falsador.
Consecuencia operativa, aplicada en este mismo documento: ninguna huella de esta revisión viaja como comentario
HTML; viajan como texto en código, en la cabecera y en el pie. Es el mismo defecto que este documento mide en los
otros: una intención declarada —el formato, la huella, el permissions:— que el entorno de ejecución no ejecuta.
Huella de esta sección: HUELLA-ADDENDUM-R2-NOTA-FORMATO-20261030.
Fuente de verdad de las cifras: INVESTIGACION/evidencia/censo_tpr_rc1_rc5_20261030.json (blob a2a7aa26,
tras las dos correcciones con cadena del 30-Oct-2026) y
INVESTIGACION/evidencia/preregistro_tpr_rc1_rc5_20261030.json (blob 05eaead8), ambos sobre los mismos 163
archivos de workflow con identidad verificada por sha256 y git hash-object.
Huella de esta revisión: HUELLA-ADDENDUM-R2-R7-LOCUS-20261030.
Huella de esta revisión: HUELLA-ADDENDUM-R2-R7-POR-ARCHIVO-20261030 y HUELLA-ADDENDUM-R2-DENOMINADOR-20261030. Los
seis archivos con R7 se verificaron uno por uno con git hash-object contra el manifiesto; el denominador de 456
sale de composicion y de los recuentos SC1 del lote, no de la prosa.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.