Desarrollo
Para contribuir o ejecutar Pitangus sin contenedores. Lee también CONTRIBUTING.es.md.
Sin contenedores
Sección titulada «Sin contenedores»Requiere Python 3.12+ y Node 22. Sin Docker no corren los motores (Trivy, Gitleaks, Opengrep): el panel lo indica en cada análisis.
make dev-setup # .venv con las dependencias de Python y node_modules del panelmake web # compila el panel en pitangus/app/static/make dev # servidor en http://127.0.0.1:8767 con datos en .dev/ (no toca los de Docker)make check # pruebas y contratos de arquitectura del backend + tipos y lint del panelmake dev usa el puerto 8767 y la carpeta .dev/ para que puedas tenerlo a la vez que la instancia de Docker. Para recarga en caliente del panel, cd web && pnpm run dev (Vite reenvía /api al backend).
La CLI usa el mismo almacén que el panel (en Docker: make cli ARGS="…"):
.venv/bin/python -m pitangus --data-dir .dev/data sources.venv/bin/python -m pitangus --data-dir .dev/data scan-repository --source-id github:org/repo.venv/bin/python -m pitangus --data-dir .dev/data runsPaginación de la API
Sección titulada «Paginación de la API»GET /api/runs/page?limit&offset&status&type&q filtra, cuenta y pagina en PostgreSQL sobre la fila ligera de cada ejecución (columna row de runs); con mil ejecuciones no se cargan mil registros con sus hallazgos. El panel usa el mismo hook de paginación en la lista de análisis, el selector de ejecución y la tabla de hallazgos.
Los registros salen por la salida de errores, legibles por defecto o como un objeto JSON por evento con PITANGUS_LOG_FORMAT=json (hora, nivel, componente, identificador de ejecución, método, ruta, estado, duración). PITANGUS_LOG_FILE=logs/app.log guarda además una copia JSON en la carpeta de datos, rotada a 10 MB × 5 (Compose lo activa). PITANGUS_LOG_LEVEL=DEBUG para depurar. No se registran cuerpos, cabeceras ni tokens, y redact() tacha patrones de credenciales que pudieran colarse en un mensaje.
Desarrollo y pruebas
Sección titulada «Desarrollo y pruebas»El frontend React/TypeScript usa shadcn/ui, Tailwind y Lucide. El selector de tema es un componente shadcn; soporta sistema, claro y oscuro (oscuro por defecto).
corepack enable # una vez: pnpm en la versión que fija web/package.jsoncd webpnpm install --frozen-lockfilepnpm run buildpnpm run lintpnpm testcd ...venv/bin/python -m unittest discover -s tests -vEl panel y el sitio de documentación usan pnpm, no npm. Su pnpm-workspace.yaml tiene los ajustes de cadena de suministro: una versión debe tener una semana antes de instalarse, se rechaza una versión reciente que pierde la procedencia que tenían las anteriores, las dependencias transitivas no pueden venir de git ni de tarballs, y los scripts de instalación de las dependencias nunca se ejecutan (uno nuevo hace fallar la instalación hasta que se lista a propósito). Si un arreglo de seguridad necesita una versión con menos de una semana, entra como excepción para esa versión exacta (minimumReleaseAgeExclude), con el aviso y la fecha en que deja de hacer falta.
Las pruebas del panel (pnpm test, vitest con Testing Library) van junto a lo que prueban (*.test.tsx) y cubren el inicio de sesión, el triage y el lanzamiento de un análisis. Sustituyen fetch por mockApi (web/src/shared/test/) y buscan los elementos por el texto del catálogo, así que cambiar un texto no las rompe.
Las pruebas necesitan PostgreSQL: make test arranca uno efímero en Docker (datos en memoria) y da a cada prueba su propio esquema (PITANGUS_DB_ISOLATE=data-dir). Para correr una sola: PITANGUS_DATABASE_URL=$(sh scripts/test-db.sh) PITANGUS_DB_ISOLATE=data-dir PITANGUS_CONFIG_DIR=$(mktemp -d) .venv/bin/python -m unittest discover -s tests -p 'test_x.py' (la carpeta de configuración temporal evita que las pruebas creen una clave maestra en la tuya). Cada make test usa su propia base de datos, que se borra al terminar, y apunta Docker a un socket que no existe: ninguna prueba puede lanzar un motor real, simulan lo que necesitan. make lint-py pasa ruff y mypy, y make arch los contratos de arquitectura; la CI corre los tres. mypy se salta los módulos listados en pyproject.toml, que tenían errores de tipos cuando llegó: arreglar uno es sacarlo de la lista.
Dependencias de Python
Sección titulada «Dependencias de Python»Lo que se edita es requirements.in (las de ejecución: lo que entra en la imagen) y requirements-dev.in (herramientas y pruebas, encima de las de ejecución). requirements.txt y requirements-dev.txt son locks que se generan a partir de ellos: cada paquete, también los transitivos, fijado con los hashes de todos sus ficheros publicados (wheels de Linux amd64/arm64 y macOS, y el sdist), así que el mismo lock sirve para la imagen, la CI y tu Mac. La imagen, la CI y make dev-setup instalan con pip install --require-hashes --only-binary :all:: si un hash no coincide o falta un paquete en el lock, la instalación se detiene, y nunca se compila nada desde el código fuente.
make lock # tras editar un .in: recompila los dos locksmake lock ARGS="--upgrade-package fastapi" # sube un paquete a la última versión que permite su rangomake lock ARGS="--upgrade" # lo actualiza todo, dependencias transitivas incluidasmake lock ejecuta pip-compile (pip-tools, con --generate-hashes) dentro del mismo python:3.12-slim-bookworm de la imagen, fijado por digest: solo necesita Docker y resuelve igual que la build de una release. Un .txt no se edita a mano; se sube junto con su .in. Dependabot (.github/dependabot.yml) propone cada semana actualizaciones de lo que nombran los .in y recompila los locks de la misma forma; las transitivas solo se mueven con make lock ARGS="--upgrade" o con una actualización de seguridad.
Añadir o migrar una ruta de la API
Sección titulada «Añadir o migrar una ruta de la API»Las rutas nuevas van en FastAPI, en pitangus/app/api/<contexto>.py: parámetros y respuesta con modelos Pydantic,
seguridad con guard(Policy(public=…, admin=…, action=…)) (CSRF, sesión, segundo factor, rol; ver
app/api/security.py) y la lógica en el módulo de negocio, nunca en la ruta. El cuerpo de la petición se lee después
de la protección con deps.body(Modelo, mensaje_inválido), así una petición sin sesión nunca llega a validarse.
Toda ruta JSON declara su response_model. Para un resultado amplio (una ejecución, el estado de GitHub), hereda de
Open en app/api/schemas.py con los campos en los que se apoyan los lectores y pasa **AS_RETURNED: el resto de
campos sale tal como lo devolvió el módulo, y uno que falte nunca se añade como null. Toda lista declara su máximo
(Field(max_length=…)), el límite del propio módulo si lo tiene: el modelo de respuesta lo comprueba, y
tests/test_api.py falla ante un array sin cota.
Después, make openapi regenera el esquema y los tipos TypeScript del panel (web/src/shared/api/), que se usan con
apiGet('/api/…'): si la API y el panel no cuadran, falla tsc. El CI comprueba que el esquema está al día.
Añadir un motor
Sección titulada «Añadir un motor»Un motor del análisis de código es un Engine (pitangus/modules/scanning/engines.py): su clave, si sin él la
ejecución queda incompleta, el mensaje de progreso que se dice antes de correrlo y cómo corre sobre un ScanContext
(snapshot, carpeta de datos, feeds de KEV/EPSS, ajustes de secretos, permiso para salir de la máquina). Para añadir uno:
- Su imagen fijada por digest en
IMAGES, y el binario en la imagen del worker con los motores dentro (docker/app/Dockerfile,worker-standalone;tests/test_packaging.pycomprueba que cuadran). - Una función
run_*que responde unEngineResult:inconclusivecon el motivo cuando no puede correr, nunca una excepción. - Su línea en
CODE_ENGINES(pitangus/modules/scanning/repository.py), en el orden en que corre. Si se solapa con otro motor, la fusión que une sus hallazgos va después de correr los motores, y el motor esmerged.
Cambiar el esquema de la base de datos
Sección titulada «Cambiar el esquema de la base de datos»Las tablas se definen en pitangus/modules/<contexto>/tables.py. Un cambio lleva su migración de Alembic:
PITANGUS_DATABASE_URL=… .venv/bin/python -c "from alembic import command; from pitangus.app.database import config; command.revision(config(), message='qué cambia', autogenerate=True)"Revisa el archivo generado en pitangus/app/alembic/versions/. tests/test_database.py falla si las tablas del código y
las migraciones no coinciden.
Cambiar el formato de datos que ya existen
Sección titulada «Cambiar el formato de datos que ya existen»Quien actualiza Pitangus ya tiene datos: una versión nueva nunca debe romperlos ni pedirle que haga nada a mano.
- Lector tolerante. El código lee también el formato anterior (en un documento JSONB o una columna
record):dict.getcon valor por defecto para campos nuevos, sin suponer tipos que antes no existían. - Migración si hay que reescribir. Un cambio de tablas va en Alembic (arriba). Reescribir contenido va al final
de
MIGRATIONSenpitangus/app/data_migrations.py: idempotente, rápida en instalaciones grandes y sin reordenar ni borrar nunca una publicada (la versión es su posición). - Prueba con datos viejos en
tests/test_migrations.py(o junto al módulo).
Si el cambio solo añade un campo que puede faltar, basta con el punto 1: no hace falta migración.
pnpm run build actualiza los activos servidos por Python. Para recarga en desarrollo usa pnpm run dev; Vite reenvía /api al backend en 8766.
Textos e idiomas
Sección titulada «Textos e idiomas»Pitangus habla inglés y español. El código, los identificadores y los comentarios van en inglés; todo lo que lee
una persona (panel, errores de la API, hallazgos, guías de corrección, progreso, informes, comentarios de PR, avisos)
existe en los dos idiomas. Antes de añadir o cambiar cualquiera de esos textos, lee
.claude/skills/pitangus-i18n/SKILL.md:
- Catálogos, no literales. Panel:
web/src/shared/i18n/locales/{en,es}/<namespace>.jsoncont('…'). Servidor:pitangus/shared/i18n/locales/{en,es}/<namespace>.json. - Se guardan códigos, no frases. En el servidor,
msg("namespace.key", **params)crea un mensaje sin idioma que se muestra al leerlo, en el idioma de quien lo lee (localize,text);t()solo para lo que no se guarda. - Interpretar, no traducir. El inglés es el origen y el respaldo; el español dice lo mismo como lo diría un ingeniero de seguridad hispanohablante, nunca palabra por palabra. La skill tiene la voz y el glosario.
- Pruebas.
tests/test_i18n.py(parte demake test) comprueba que en/es tienen las mismas claves y los mismos{{params}}, y que toda clave literal usada en el código existe. Las pruebas corren conPITANGUS_DEFAULT_LOCALE=es; el inglés se comprueba de forma explícita conAccept-Language: en.
La documentación sigue la misma regla: inglés en docs/, español en docs/es/, y cada página enlaza con la otra.