Releases: proscar87/garita
Release list
v1.0.0 — El guardián que no pudo revisar ya no aprueba
In English: Garita 1.0 — a guard that could silently not run now fails loudly. Reviewing zero files used to print "nothing to report" and exit 0; it now exits 2 and says why, across all four output channels. The --linea-base path no longer deletes a committed baseline after an empty scan. Action users move from @v0 to @v1.
Por qué este es el 1.0
Porque se quitó el defecto que la descalificaba. Un guardián que puede
no correr y reportar cero es lo primero que descarta quien evalúa una
herramienta de seguridad, y hasta ayer Garita lo hacía.
El punto salió del issue #1, de un incidente real: «una comprobación que
sale vacía se lee como aprobada» apareció tres veces en un día por tres
caminos distintos. Garita tenía su propia versión.
Lo que cambia
Revisar cero archivos ya no es una aprobación. Imprimía «✓ nada que
reportar» con código 0. Ahora sale con 2 y dice cuál de los dos agujeros
es: no hay archivos rastreados, o se omitieron todos. El arreglo difiere,
así que el mensaje los distingue.
Reproducido en tres rutas, cada una peor que la anterior:
- La revisión normal.
--historial, la ruta de la auditoría programada, que decía «✓ el
historial está limpio» sobre un repositorio sin commits.--linea-base, la única destructiva: borraba la línea base
commiteada, lo anunciaba como deuda pagada y salía con 0.
Los cuatro canales lo dicen. El resumen del job llegaba a imprimir «✅
Nada que reportar» sobre una corrida que ya salía con 2, y ese panel es
lo que se mira sin abrir los registros.
La regla no es «cero revisados es error» a secas. Cuando la lista de
archivos la pone quien llama —el hook de pre-commit entrega los del
commit— cero revisados es el resultado legítimo de lo que se pidió. Un
commit que sólo toca imágenes no bloquea nada. Verificado por la ruta
real de la Action contra el caso de un consumidor que corre con
solo-cambios: true.
El falso positivo de self.x = x, medido en campo: 17 de los 20
avisos del consumidor más grande. El valor que repite el nombre asignado
es el patrón más común de un constructor, no una credencial.
Al actualizar
El tag mayor pasa de v0 a v1:
- uses: proscar87/garita@v1- repo: https://github.com/proscar87/garita
rev: v1.0.0v0 queda congelado apuntando a este mismo commit, así que nada se rompe
hoy; las versiones nuevas van a v1.
Si tu CI se pone en rojo tras actualizar y el repositorio está limpio,
es el arreglo haciendo su trabajo: Garita no estaba revisando nada. El
mensaje dice si es porque git no rastrea archivos ahí o porque se
omitieron todos.
v0.33.0 — Un número, un hallazgo
El README prometía que «un identificador con dígito verificador no valida fuera de su país, así que no dispara». Es cierto entre familias distintas y falso dentro de una misma. Medido antes de tocar nada:
| Colisión | Cruce |
|---|---|
| NIT guatemalteco ↔ RUC paraguayo (el mismo módulo 11, la misma base, las mismas palabras de contexto) | 100 % |
| CUIT argentino ↔ RUC peruano (los mismos pesos; el prefijo 20 es válido en los dos) | 82.2 % |
| Cédula ecuatoriana ↔ NIT colombiano («cédula» satisface el refuerzo del NIT base 9) | 8.3 % |
El reporte emitía dos hallazgos sobre el mismo número, cada uno afirmando una nacionalidad distinta con la misma seguridad. Las dos no pueden ser ciertas, y quien lee no tiene cómo saber cuál lo es.
Qué se hizo
La información para desambiguar no está en el número, así que no hay arreglo que la deduzca. Lo que sí se puede es dejar de fingir que la hay: cuando dos países reclaman el mismo valor en la misma línea se emite un hallazgo, con los candidatos nombrados y con el camino para volverlo inequívoco (paises:). Se conserva el de mayor severidad y, a igualdad, el primero por nombre de detector, para que la línea base y el SARIF sigan casando entre corridas.
La promesa falsa estaba en tres lugares —README, paises/__init__.py y Config.paises— y los tres quedaron corregidos con los porcentajes medidos.
Lo que casi sale mal
La regla exige países distintos, y no es un detalle: la primera versión agrupaba por (archivo, línea, valor recortado) a secas, y al medirla contra un repositorio real se comió un RFC. El valor va recortado, así que el CURP y el RFC de una misma persona —que empiezan igual y pueden terminar igual— compartían clave. Dos documentos del mismo país no se contradicen: se suman.
Verificación
322 pruebas, cuatro de ellas de contrapeso. Medido contra siete repositorios reales, incluidos los cuatro consumidores del tag v0: reportes byte a byte idénticos.
v0.32.0 — El plural también nombra, y los rellenos no son personas
Último tema del roadmap: la calibración de los dieciséis países. De los diez ítems, ocho eran reales y dos se refutaron al reproducirlos.
El plural (falso negativo)
Toda regex de contexto cerraba con \b pegado al sustantivo singular. Pero la clave de un YAML, el campo de un JSON y el nombre de una columna van casi siempre en plural —«cedulas», «rucs», «contribuyentes»—, así que los detectores con contexto obligatorio quedaban ciegos sobre justo la forma en que se exporta un padrón. Medido antes de tocar nada: cinco detectores disparaban con el singular y callaban con el plural.
Trece países corregidos. Los acrónimos que en plural chocan con una palabra común del inglés —run→runs, sii— se quedaron sin la «s» a propósito: una palabra de contexto envenenada es peor que la forma que deja de casar.
Los rellenos (falso positivo)
Los dígitos repetidos pasan el módulo 10 y el 11 con frecuencia, así que el campo vacío de un export disparaba como una persona: los ocho DNI españoles de dígito repetido, los CIF de relleno, el RUT chileno de ceros —que se escapaba porque el rango empezaba en 1—, cinco SSN, y el NIT colombiano, cuya lista de exentos estaba literalmente vacía.
Cada lista se genera con el validador de su país: una escrita a mano envejece en cuanto cambie el algoritmo, y una incompleta es la que hace que alguien apague el detector. Más el rango 987-65-432X que la SSA reserva para publicidad.
Dos refutados
- El NIT guatemalteco secuencial. Exentar las secuencias ascendentes rompió tres pruebas del propio proyecto —un CIF y un NIT colombiano legítimos usan justo esos cuerpos— porque un identificador secuencial real existe, a diferencia de uno de ocho dígitos repetidos.
- El punto del teléfono mexicano. No se pudo producir un falso positivo: el detector no dispara dentro de un número ni de una URL, y ahí el punto es refuerzo, no separador.
Verificación
314 pruebas: cinco verificadas fallando sin su arreglo y dos de contrapeso. Medido contra siete repositorios reales, incluidos los cuatro consumidores del tag v0: reportes byte a byte idénticos.
v0.31.0 — El repositorio hostil no desarma al guardián
El .garita.yml y los nombres de archivo los trae el repositorio revisado: en un pull request de un fork los escribe quien manda el PR. Seis maneras de que ese PR saliera aprobado, las seis reproducidas antes de tocar nada.
- Una exención con patrón
*o**apagaba el repositorio entero y el reporte no la nombraba: «✓ nada que reportar» con código 0 y una línea gris. No se bloquea —hay repos que legítimamente revisan una sola carpeta— pero ahora se nombra el patrón en los cuatro canales, y en el SARIF con nivel de error: no es una reserva sobre el veredicto, es la ausencia del veredicto. - Un motivo hecho sólo de espacios pasaba la validación que toda la herramienta promete obligatoria. Tres espacios no son una razón.
- El hook de pre-commit pasaba los nombres sin
--:garita --version datos.txtimprime la versión y sale con 0, o sea que un archivo llamado--versionaprobaba el commit entero. La Action cerró este ataque en su envoltorio; esta superficie se había quedado abierta. - Un nombre de archivo hecho sólo de espacio en blanco —legal en git y en POSIX— se caía de la lista en modo solo-cambios.
leer()seguía los enlaces simbólicos. Lo que git publica de un symlink es la cadena de su destino, así que la revisión normal reprobaba por contenido que no está en el repositorio —y que puede vivir en cualquier parte del disco de quien corre CI— mientras--historial, que sí lee el blob, decía «limpio» sobre el mismo commit.--linea-baseborraba la línea base commiteada cuando faltaba una fuente opcional, y lo anunciaba como «deuda ya pagada». Cero hallazgos ahí no es deuda pagada: es revisión incompleta.
Verificación
307 pruebas: seis verificadas fallando sin su arreglo y dos de contrapeso. Medido contra siete repositorios reales, incluidos los cuatro consumidores del tag v0: reportes byte a byte idénticos.
v0.30.1 — NTFS prohíbe la barra, igual que los dos puntos
Arreglo de pruebas. Ningún cambio de comportamiento.
La prueba del escapado de la tabla del resumen creaba un archivo llamado tubo|corte.py, y en Windows eso no se puede ni crear: OSError 22 antes de llegar a la aserción. Tumbó los tres casos de la clase en windows-latest y dejó CI en rojo con v0.30.0 ya publicada.
Es el mismo molde que los dos puntos en v0.15.0, y ahí la salida fue saltarse Windows. Esta vez no: el escapado se prueba llamando a resumen_markdown con hallazgos construidos a mano, sin tocar el disco, así que corre en las cinco plataformas — incluida la única donde el carácter es ilegal, que es justo la que un skip habría dejado sin cubrir.
Verificado que la prueba nueva falla contra el reporte.py de v0.29.0. 299 pruebas.
v0.30.0 — Los cuatro canales dicen lo mismo
Garita reporta por cuatro canales —terminal, resumen del job, HTML y SARIF— y un hallazgo que sale en uno y no en otro es un hallazgo que alguien no verá. Los ocho ítems de este tema del roadmap, reproducidos uno por uno.
El HTML
Era el más callado, y es el que su propio docstring describe como el entregable para el cliente, el auditor, el consejo directivo: no mencionaba los archivos ilegibles, ni los detectores que la configuración apagó, ni las exenciones muertas. Ahora los dos reportes HTML llevan una sección Reservas sobre este veredicto.
El SARIF
- Callaba los recortes de configuración: cero alertas sobre un repositorio con la mitad del guardián apagado.
- Callaba las exenciones muertas.
--historialno declaraba nada de lo no revisado: «cero alertas» sobre un blob de megas que nunca se abrió, en el modo cuyo propósito entero es no dar nada por revisado.- Las huellas ahora distinguen alertas del mismo archivo; sin eso las tres ancladas a
.garita.ymlse fundían en una.
El resumen del job
- No llevaba severidad: el error y el aviso se veían idénticos, así que quien mira el panel no podía saber cuál rompe el build.
- Una barra en el nombre del archivo partía la fila —y la parte aunque esté dentro de un span de código—. Cinco celdas contra un encabezado de cuatro: GitHub truncaba y el enlace apuntaba a una ruta inexistente.
--historialno lo escribía nunca, así que el panel de la auditoría programada salía vacío.
Y el «qué» de una llave privada
Era ----…----: recortar toma los extremos, y los dos extremos de un PEM son guiones. Ahora se nombra por su etiqueta —ENCRYPTED PRIVATE KEY—, que distingue el arreglo y no es el secreto: el secreto es el cuerpo, y el cuerpo no se imprime nunca.
Verificación
298 pruebas; las seis nuevas verificadas fallando sin su arreglo. Medido contra los cuatro consumidores del tag v0: la terminal sale idéntica en los cuatro y el SARIF gana exactamente una alerta — una exención muerta real que hasta hoy sólo se veía en la terminal.
v0.29.0 — La exención nombra exactamente lo que exenta
El séptimo hallazgo de la séptima oleada, aparte porque cambia la semántica de casa_ruta.
casa_ruta implementaba dos de las tres reglas de gitignore. Faltaba el anclaje, y era el único caso que el usuario no podía expresar: config.json exentaba también el de cualquier subcarpeta, y ni ./config.json ni /config.json casaban nada.
Eso hacía que --proponer-exenciones emitiera, para un archivo de la raíz, un patrón más ancho que el hallazgo que lo motivó: quien pegaba el bloque creyendo exentar vectores.json exentaba además el de cualquier subcarpeta —hoy o el día que alguien lo agregue— sin verlo, porque la exención sí aplicaba y no salía como exención muerta.
Ahora una barra inicial ancla a la raíz, y la propuesta la emite anclada.
Compatibilidad
Estrictamente aditivo: hasta hoy un patrón con barra inicial no casaba nada y salía denunciado como exención muerta, así que ningún .garita.yml existente cambia de comportamiento. Verificado contra diez repositorios reales —reportes byte a byte idénticos— y contra los cuatro consumidores del tag v0, donde ninguna de las once exenciones escritas usa barra inicial.
292 pruebas, incluida una explícita de que las otras dos reglas de gitignore no se movieron: anclar todo patrón fue la regresión de v0.18.0 que rompió *.test.ts.
v0.28.0 — Seis maneras de aprobar sin haber mirado
La séptima oleada de auditoría encontró siete defectos confirmados; tres nacieron en las versiones de ayer. Van seis en esta versión.
Aprobaban con código 0
- La llave de cuenta de servicio de Google dejó de detectarse en v0.27.0. El filtro de «frase» contaba tokens, no palabras: un JSON de service account minificado en una línea llega a ocho antes de
private_keyy se descartaba como documentación. detectores: []pasó de no exentar nada a exentar TODO. Se lee con el ojo como «ninguno» y silenciaba el archivo completo. Ahora se rechaza con código 2.- Un archivo rastreado y ausente del árbol de trabajo se omitía en silencio bajo la etiqueta «binario o muy grande». Con
git sparse-checkout—queactions/checkoutsoporta— el contenido sigue en HEAD y el push lo publica. Ahora se lee del índice.
Cegueras más viejas
ENCRYPTED PRIVATE KEYnunca había sonado, y es lo que emitenopenssl genpkey -aes256,genrsa -aes256ypkcs8 -topk8. TampocoDSA, y elPGPque el patrón anunciaba era letra muerta.- Los tokens de prefijo punteado —
hvs.de Vault,dp.st.de Doppler,cs.live.— no se veían en un.env, que es justo donde viven. - La rama del BOM no usaba el respaldo cp1252 por byte, y el «CSV UTF-8» de Excel siempre escribe BOM.
Verificación
289 pruebas. Las seis fallan sin su arreglo. Medido contra diez repositorios reales —los cuatro consumidores del tag v0, twilio-python y cinco más, unos 9 000 archivos—: reportes byte a byte idénticos, cero ruido nuevo.
v0.27.0 — La cabecera PEM no es la llave
El arreglo más rentable del roadmap, y el primero que sale del frente de ruido — la mitad de la doctrina que llevábamos seis oleadas sin medir.
llave_privada casaba la cabecera PEM sin exigir que hubiera llave debajo. Medido sobre twilio-python: 48 hallazgos, los 48 documentación. Ahora 0, y ese repositorio pasa de rojo a verde, que era el veredicto correcto.
Las señales que separan una llave de su mención resultaron ser tres:
- La cabecera sin cuerpo no es una llave (una mención en un docstring, un comentario que explica el formato).
- En la misma línea, el cuerpo va pegado a la cabecera o tras un salto escapado —
KEY-----\nMIIE…, como se guarda en un.envo un JSON—, nunca tras un espacio: un PEM de verdad lleva salto de línea ahí, y lo que se ve con espacios es el manual enseñando el formato con una llave recortada. - Y si delante hay una frase (seis palabras o más), es documentación: una llave real vive tras
KEY=","key": "oprivate_key = ".
Las diez formas reales siguen sonando, verificadas una por una: PEM normal, cifrada con sus cabeceras RFC 1421 (Proc-Type, DEK-Info), OPENSSH, en .env con \n y con \r\n, en JSON pegada, en asignación y en una llamada con varios argumentos.
279 pruebas.
v0.26.1 — La lista vacía es una lista
exenciones: [] se leía como la cadena "[]" y el validador lo rechazaba con «cada exención necesita archivo y motivo» — sobre una lista que no tiene ninguna. El único repositorio que no compilaba era justo el que declara explícitamente «aquí no hay exenciones», que es el caso que hay que premiar.
El arreglo estaba en main desde ayer pero no en el tag flotante v0, así que quien consume la Action por @v0 seguía con el bug. Esta versión lo lleva.
Incluye también una nota de vocabulario en el README: las exenciones son lo que otras herramientas llaman excepciones, ignores o allowlist, y la sección en inglés ahora trae un glosario de las claves del YAML.
277 pruebas.