Desarrollo¶
Entorno¶
git clone https://github.com/Eche-Dev/Gatekeeper.git
cd Gatekeeper/gatekeeper-v0.3/gatekeeper
pip install -e ".[dev]"
Python 3.10+ (el CI prueba 3.10–3.13). El proyecto vive en
gatekeeper-v0.3/gatekeeper/; v0.1 y v0.2 son históricas.
Comandos¶
| Comando | Qué hace |
|---|---|
ruff check . |
Estilo |
mypy |
Tipos estrictos (src/gatekeeper, target 3.10) |
bandit -r src -q |
Análisis estático de seguridad |
pytest --cov=gatekeeper |
Tests + fuzzing, cobertura ≥ 90% |
python tools/evaluate.py --check |
Tasas de detección y FP (corpus propio + CRS) |
python tools/import_crs.py --check |
Procedencia del corpus externo |
python benchmarks/latency.py |
Latencia p50/p95/p99 por petición y escenario |
python benchmarks/latency.py --baseline <json> |
Compara con una Medición previa (misma máquina) |
python -m build && twine check dist/* |
Empaquetado |
Estructura¶
| Carpeta | Contenido |
|---|---|
src/gatekeeper/ |
El middleware: middleware, config, rules, body, paths, ratelimit, cli |
tests/ |
Unitarios, test_corpus.py, test_crs_corpus.py, test_fuzz.py |
corpus/ |
attacks.json, benign.json, crs_payloads.json (CRS, Apache-2.0) e informes |
tools/ |
evaluate.py (medición) e import_crs.py (importador de CRS) |
benchmarks/ |
latency.py |
docs/ |
Esta documentación (MkDocs Material) |
Tests: tres capas¶
- Unitarios — comportamiento por componente, incluida la resistencia a ReDoS con entradas patológicas de 1M de caracteres.
- Corpus —
evaluate.pydentro de pytest: la detección no puede bajar del 90% ni aparecen FP sin documentar; lo mismo con la línea base de CRS. - Propiedades (hypothesis) —
tests/test_fuzz.py: entradas arbitrarias (unicode, percent-encoding, bytes) sobrescan,normalize,extract_strings,Configyallow_paths. Invariantes: nada revienta,normalizees idempotente, y una ruta con..nunca se exime.
Latencia: medir, no construir un guard automático¶
benchmarks/latency.py mide percentiles por petición real (p50/p95/p99), no
medias por ronda: la media escondía la cola, que es lo que nota un usuario. El
mismo escenario que la media rondaba los ~90 µs tiene un p95 de 123 µs.
Como comparar dos versiones¶
python benchmarks/latency.py --save base.json
# ... cambia el código ...
python benchmarks/latency.py --baseline base.json # sale 1 si empeoró más del margen
El margen por defecto es 25 %. Compara el overhead (lo que añade el middleware sobre la línea base de la misma corrida), no el absoluto.
El pre filtro de familias¶
Antes de evaluar las 60 regex, Gatekeeper comprueba si el texto contiene algún
literal de cada familia (script, union, /etc/…) con una búsqueda de
subcadena. Si no, esa familia se salta entera. La normalización cuesta ~5 µs;
el resto eran regex de Python sobre texto inocuo.
Se midieron cuatro enfoques y solo uno gana, porque la diferencia entre ellos es de 2× a 4×:
| Enfoque | Coste medido | Veredicto |
|---|---|---|
11 search, uno por familia |
~12 µs | se come el ahorro |
finditer con alternancia gigante |
15 µs corto, 80 µs largo | crea un objeto por coincidencia |
search con grupos por familia, en bucle |
~30 µs | el bucle se come todo |
lit in texto (el implementado) |
~1 µs | in sobre str es C puro |
Al añadirlo, el test que compara con y sin pre filtro sobre los dos corpus
encontró 12 discrepancias (literales mal escritos que habrían perdido
detecciones). Esa es la razón de que exista tests/test_prefilter.py: la
optimización solo es aceptable si se puede demostrar que no cambia el
resultado.
Por qué no corre en CI¶
Se implementó un guard automático con umbral en µs y se eliminó al medir que no podía ser fiable. En un runner de CI con CPU compartida, el ruido de la máquina supera a la señal:
| Situación | Overhead p50 medido |
|---|---|
| Portátil sin carga | 73 µs |
| Runner de CI saturado | 220–312 µs |
| Regresión real (200 firmas nuevas) | 175 µs |
Una máquina lenta da un overhead mayor que una regresión de código: ningún
umbral los separa. El guard solo podría dar falsos positivos en cada build o
ser ciego a regresiones moderadas. La decisión y sus datos están también en el
ci.yml, para que nadie lo reintroduzca sin conocer la razón.
El benchmark sigue siendo la herramienta: publicar la cola real y comparar antes/después localmente. Automatizarlo exigiría un runner con CPU fijada.
Documentación¶
La publicación es el job deploy del workflow Docs: corre en cada push a
main, al etiquetar un tag o manualmente desde la pestaña Actions. Requiere
Settings → Pages → Source: GitHub Actions en el repo.
--strict hace que un enlace roto rompa el build en CI.
Añadir una regla¶
El proceso completo (firma genérica, medición, cuantificadores acotados,
documentar límites) está en
CONTRIBUTING.md.
Resumen: cámbiala en rules.py, ejecuta evaluate.py y los tests, y añade los
casos benignos que no deben saltar.
Release¶
CHANGELOG.md(Keep a Changelog) y versión enpyproject.toml.- Batería completa verde (el checklist del PR).
git tag vX.Y.Z && git push --tags→ publica docs.- Crear la release en GitHub → dispara el workflow
publicar.yml, que valida y publica en PyPI.
Publicar en PyPI (Trusted Publishing)¶
No hay ningún token de PyPI en los secretos del repo. GitHub demuestra su identidad a PyPI con OIDC y PyPI emite un token de publicación de un solo uso. Lo único que puede publicar con tu nombre es quien pueda modificar el repo.
El workflow valida antes de publicar: que la versión del tag coincida con
pyproject.toml, que el paquete importe y que la CLI funcione. Para probarlo
sin publicar nada, desde la pestaña Actions: Publicar en PyPI → Run workflow →
dry_run = true.
Configuración inicial (una sola vez, a mano):
- Crear el proyecto
gatekeeper-asgien PyPI. - Añadir un publisher GitHub Actions con repositorio
Eche-Dev/Gatekeepery workflowpublicar.yml. - La primera versión hay que subirla a mano con un token de API (OIDC no crea el proyecto en PyPI, solo publica en uno que ya existe) y borrar el token después.
Auditar dependencias¶
pip-audit corre en el job audit de CI sobre el runtime y el entorno de
desarrollo, en venvs limpios. En local, lo mismo:
python -m venv /tmp/audit && /tmp/audit/bin/pip install dist/*.whl
/tmp/audit/bin/pip install pip-audit
/tmp/audit/bin/pip freeze | grep -v '^-e ' | grep -vi gatekeeper-asgi > /tmp/req.txt
/tmp/audit/bin/pip-audit --strict -r /tmp/req.txt
Se filtra el paquete local porque gatekeeper-asgi ya está publicado en
PyPI y --strict sale con error al encontrarlo instalado en editable desde
el repo. Eso no es una vulnerabilidad: es el paquete local.