Un pipeline de GitHub Actions que escanea con SAST antes de cada despliegue, una app FastAPI + Oracle y una lección sobre cómo separar el ruido de la señal.
Introducción
La primera vez que abrimos bandit-report.json nos llevamos un susto: 467 hallazgos en un proyecto de unas 3 900 líneas. Nos tomó un rato entender que ese número, por sí solo, decía muy poco. Este artículo cuenta cómo llegamos ahí, integrando Bandit en un pipeline de CI/CD, y sobre todo cómo aprendimos a interpretar lo que nos devolvió.
El proyecto en dos minutos
Somos dos estudiantes de Ingeniería de Sistemas de la Universidad Privada de Tacna. Para el curso de Calidad y Pruebas de Software desarrollamos un Framework de Pruebas de Base de Datos SQL (un prototipo académico, sin cliente real): una aplicación web hecha con FastAPI y orientada a Oracle, en la que un analista QA registra una consulta, indica qué espera obtener, la ejecuta y revisa después qué ocurrió.
| Función | Cómo funciona |
|---|---|
| Perfiles de conexión | Guardan host, puerto, usuario y service name. La contraseña nunca se persiste: se pide al probar o ejecutar |
| --- | --- |
| Casos de prueba | Una sentencia SQL, un resultado esperado y un tipo de validación (ROW_COUNT o EXISTS) |
| --- | --- |
| Suites | Conjuntos de casos del mismo proyecto que se ejecutan juntos |
| --- | --- |
| Veredicto | PASS, FAIL o ERROR por cada ejecución |
| --- | --- |
| Historial y dashboard | Registro consultable de cada corrida, con filtros por proyecto, suite, caso y estado |
| --- | --- |
Como la herramienta ejecuta SQL de verdad, le pusimos controles propios: una sola sentencia por caso, DDL y TCL bloqueados, ROLLBACK obligatorio después de todo DML y reglas distintas por ambiente (TEST; STAGING, que pide confirmación; y PRODUCTION, solo lectura y con las filas ocultas). La autenticación usa una cookie HttpOnly con un token firmado.
Nada de esto nos exime de revisar nuestro propio código. Un ROLLBACK no cubre todos los casos (Oracle hace commit implícito con DDL y permite transacciones autónomas) y sqlparse, que usamos para reconocer sentencias, no pretende ser un validador. Por eso sumamos otra capa.
¿Por qué SAST?
Una forma sencilla de verlo: SAST (Static Application Security Testing) es un revisor de código que no se cansa y que lee cada archivo sin necesidad de ejecutar la aplicación. Encuentra cierto tipo de errores, como credenciales escritas en el código, excepciones que se tragan los fallos o llamadas a procesos del sistema, mientras todavía es barato corregirlos.
Para Python elegimos Bandit. Analiza el árbol sintáctico (AST) de cada archivo, aplica reglas con identificadores como B105 o B110, relaciona cada hallazgo con un CWE y genera un reporte JSON. OWASP lo incluye en su catálogo de herramientas de análisis de código fuente, aunque aclara que no respalda ninguna en particular. También tiene límites claros: detecta patrones, no entiende la lógica de negocio y no mira las dependencias.
Arquitectura del despliegue
Podemos pensar el despliegue en tres capas:
- Código, en GitHub. El repositorio es público: https://github.com/Geraldzvallos/framework-pruebas-sql-publico
- Automatización, en GitHub Actions. Cada push a la rama main ejecuta el escáner de seguridad y, solo si pasa, actualiza el despliegue.
-
Ejecución, en Microsoft Azure. Una máquina virtual Linux con Docker Compose que aloja:
- Caddy, que termina HTTPS y expone los puertos 80 y 443.
- Un contenedor con FastAPI, que sirve la API REST y la interfaz web.
- SQLite con migraciones de Alembic para los metadatos y el historial.
- Oracle Free como base objetivo de la demostración. La API y Oracle se hablan por la red interna de los contenedores.
La aplicación está en línea aquí: https://frameworksql.sytes.net/login
┌──────────┐ push ┌──────────────────────────────┐ SSH ┌───────────────────────────┐
│ GitHub │ ───────▶ │ GitHub Actions │ ───────▶ │ VM de Azure (Docker) │
│ (main) │ │ 1) Bandit 2) ¿pasa? 3) CD │ │ Caddy → FastAPI → Oracle │
└──────────┘ └──────────────────────────────┘ └───────────────────────────┘
Una precisión importante: Oracle Free sirve para demostrar, pero el fabricante no ofrece soporte ni parches para esa edición. En un entorno empresarial usaríamos una edición soportada.
Implementación de la automatización
Primero, a mano
Antes de tocar el pipeline probamos Bandit en nuestra máquina:
pip install bandit
bandit -r . -f json -o bandit-report.json
Queríamos la salida en JSON porque es un artefacto que se puede guardar, comparar entre ejecuciones y procesar con un script.
Después, el job de seguridad
name: Security Scan and Deploy
on:
push:
branches:
- main
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install Bandit (SAST Tool)
run: pip install bandit
- name: Run Security Scan
run: bandit -r ./framework-pruebas-sql -f json -o bandit-report.json || true
- name: Upload Security Report
uses: actions/upload-artifact@v4
with:
name: sast-report
path: bandit-report.json
La decisión más útil aquí fue enfocarnos en generar el artefacto. Con la acción upload-artifact, GitHub guarda el bandit-report.json de cada ejecución. Así tenemos la evidencia descargable de las vulnerabilidades lista para ser procesada
Un ajuste que nos habría ahorrado ruido
Nuestro escaneo apuntó a la raíz del repositorio, así que también analizó tests/. Es posible fijar el alcance desde pyproject.toml:
[tool.bandit]
exclude_dirs = ['tests']
pip install 'bandit[toml]'
bandit -c pyproject.toml -r .
Resultados del escaneo
El reporte, generado por nuestro pipeline el 3 de octubre de 2026 con Bandit 1.9.4, cubrió 3 936 líneas de código.
- Total: 467 hallazgos.
- Severidad: 0 altos, 1 medio y 466 bajos.
- Confianza: 427 altos y 40 medios.
La lista por regla:
- B101 (assert_used): 410
- B105 (hardcoded_password_string): 25
- B106 (hardcoded_password_funcarg): 15
- B110 (try_except_pass): 5
- B603 (subprocess_without_shell_equals_true): 5
- B607 (start_process_with_partial_path): 4
- B404 (import de subprocess): 2
- B310 (urllib.urlopen): 1, el único de severidad media
Leer el reporte en tres pasos
- Severidad. Ninguno es alto. Ya con eso se desinfla el susto inicial.
- Ubicación. De los 467 hallazgos, 461 (98,7 %) están en tests/. Solo 6 afectan a código que no es de pruebas.
- Contexto. Un mismo patrón puede ser grave en producción e irrelevante en un test. Bandit no sabe distinguirlos; nosotros sí.
Los que sí tocan código que no es de pruebas
B110: excepciones silenciadas (5 casos). Dos están en el motor de ejecución, app/engine/executor.py, y tres en la migración 001 de Alembic:
try:
self._connection.close()
except Exception:
pass
El riesgo en producción es la invisibilidad. Si cerrar una conexión o un cursor falla de forma recurrente, nadie se entera hasta que se agotan los recursos, y el síntoma aparece lejos de su causa. En la migración, un drop_constraint que falla sin avisar puede dejar el esquema a medias. Lo corregiríamos capturando el error específico del driver y registrándolo:
except oracledb.Error:
logger.warning('No se pudo cerrar la conexión', exc_info=True)
B310: urlopen sin validar el esquema (1 caso, severidad media). Aparece en scripts/validar_flujo_oracle.py. La función urlopen también acepta esquemas como file://, de modo que, si una URL llegara a estar bajo control de un tercero, podría leerse un archivo local (CWE-22). En un script interno el riesgo real es bajo, pero validar el esquema es barato:
if urlparse(url).scheme not in {'http', 'https'}:
raise ValueError('Solo se permiten URLs http o https')
Los que parecen graves, pero aquí no lo son
B105 y B106: contraseñas en el código (40 casos). Son los más llamativos:
executor = TargetDatabaseExecutor(dsn='localhost/xe', user='user', password='secret_password')
Una credencial real en el código queda para siempre en el historial de Git y, en un repositorio público, a la vista de cualquiera (CWE-259). Es de las fallas más frecuentes y más fáciles de aprovechar. En nuestro caso, los 40 hallazgos están en archivos de prueba y son valores ficticios, como secret_password o super_secret_password_999. Aun así, vale la pena ordenarlos (constantes, fixtures y un # nosec B105 con justificación), porque en un reporte con tantos falsos positivos el día que aparezca una credencial real será fácil pasarla por alto.
B603, B607 y B404: procesos del sistema (11 casos). Los tests invocan Alembic con subprocess:
subprocess.run(['alembic', 'upgrade', 'head'], env=env, check=True)
B603 pide comprobar que ninguna entrada no confiable llegue al comando (CWE-78) y B607 advierte que el ejecutable se resuelve por nombre, de modo que alguien que controle el PATH podría colar otro programa. Aquí los argumentos son constantes y no hay shell=True, por lo que el riesgo es bajo. Aun así, resolvemos ambas alertas fijando el intérprete:
subprocess.run([sys.executable, '-m', 'alembic', 'upgrade', 'head'], env=env, check=True)
B101: 410 assert (el ruido puro). Bandit recuerda que los assert se eliminan al ejecutar Python con -O. Sería un problema si protegieran una regla de seguridad; en pytest son la forma normal de comprobar. Son el 88 % del reporte y no hay nada que accionar.
Conclusión y aprendizajes
- Un número grande no es un riesgo grande. Cero hallazgos altos y seis fuera de tests/ cuentan una historia muy distinta a la del titular de 467.
- El contexto es parte del análisis. Bandit detecta patrones; saber si el patrón vive en un test o en el motor de ejecución es trabajo humano.
- Una puerta que bloquea todo se desactiva. Bloquear por severidad alta e informar el resto mantuvo el pipeline útil y respetado.
- El alcance se diseña. Excluir tests/ de la puerta, o escanearlos aparte de forma informativa, habría reducido el reporte de 467 a 6 hallazgos.
- SAST es una capa más. No sustituye la auditoría de dependencias (por ejemplo, pip-audit), las pruebas negativas de nuestra política SQL ni los permisos mínimos en Oracle.
El siguiente paso para nosotros es corregir los B110 y el B310, fijar el alcance en pyproject.toml y sumar la revisión de dependencias al mismo pipeline.
Video demostrativo
Puedes ver el flujo completo, desde el push hasta el despliegue automático, en este video:
- Aplicación: https://frameworksql.sytes.net/login
- Repositorio: https://github.com/Geraldzvallos/framework-pruebas-sql-publico
Top comments (0)