Ir al contenido

Desarrollo

Para contribuir o ejecutar Pitangus sin contenedores. Lee también CONTRIBUTING.es.md.

Requiere Python 3.12+ y Node 22. Sin Docker no corren los motores (Trivy, Gitleaks, Opengrep): el panel lo indica en cada análisis.

Ventana de terminal
make dev-setup # .venv con las dependencias de Python y node_modules del panel
make 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 panel

make 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="…"):

Ventana de terminal
.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 runs

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.

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).

Ventana de terminal
corepack enable # una vez: pnpm en la versión que fija web/package.json
cd web
pnpm install --frozen-lockfile
pnpm run build
pnpm run lint
pnpm test
cd ..
.venv/bin/python -m unittest discover -s tests -v

El 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.

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.

Ventana de terminal
make lock # tras editar un .in: recompila los dos locks
make lock ARGS="--upgrade-package fastapi" # sube un paquete a la última versión que permite su rango
make lock ARGS="--upgrade" # lo actualiza todo, dependencias transitivas incluidas

make 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.

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.

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:

  1. 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.py comprueba que cuadran).
  2. Una función run_* que responde un EngineResult: inconclusive con el motivo cuando no puede correr, nunca una excepción.
  3. 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 es merged.

Las tablas se definen en pitangus/modules/<contexto>/tables.py. Un cambio lleva su migración de Alembic:

Ventana de terminal
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.

Quien actualiza Pitangus ya tiene datos: una versión nueva nunca debe romperlos ni pedirle que haga nada a mano.

  1. Lector tolerante. El código lee también el formato anterior (en un documento JSONB o una columna record): dict.get con valor por defecto para campos nuevos, sin suponer tipos que antes no existían.
  2. Migración si hay que reescribir. Un cambio de tablas va en Alembic (arriba). Reescribir contenido va al final de MIGRATIONS en pitangus/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).
  3. 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.

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>.json con t('…'). 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 de make 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 con PITANGUS_DEFAULT_LOCALE=es; el inglés se comprueba de forma explícita con Accept-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.