De git push a producción sin sorpresas: SAST con Bandit en un pipeline de GitHub Actions
Cómo integramos análisis estático de seguridad en el despliegue continuo de una aplicación FastAPI + Oracle sobre Azure, y qué aprendimos al leer 467 hallazgos. 1. Introducción Qué construimos Somo
Cómo integramos análisis estático de seguridad en el despliegue continuo de una aplicación FastAPI + Oracle sobre Azure, y qué aprendimos al leer 467 hallazgos.
1. Introducción
Qué construimos
Somos dos estudiantes de Ingeniería de Sistemas de la Universidad Privada de Tacna y, para el curso de Calidad y Pruebas de Software, construimos un Framework de Pruebas de Base de Datos SQL: una aplicación web para definir, ejecutar y registrar pruebas SQL sobre bases de datos Oracle. Es un prototipo académico, sin un cliente empresarial detrás, y justamente por eso quisimos tratarlo con las prácticas que aplicaríamos en un proyecto real.
El problema es fácil de enunciar. Cuando se modifica una consulta o una operación sobre los datos, hay que comprobar que sigue entregando lo esperado; si esa comprobación queda como una ejecución aislada, después cuesta repetirla o compararla con una anterior. Nuestra aplicación reúne en un solo lugar la sentencia, la conexión, el resultado esperado y el registro de cada ejecución.
Hoy permite:
- Gestionar proyectos y perfiles de conexión a Oracle. Un perfil guarda host, puerto, usuario y service name, pero nunca la contraseña: se solicita solo al probar la conexión o ejecutar.
- Definir casos de prueba con dos validaciones: ROW_COUNT (número de filas esperado) y EXISTS (una condición verdadera o falsa).
- Agrupar casos en suites del mismo proyecto.
- Ejecutar y clasificar cada resultado como PASS, FAIL o ERROR.
- Consultar el historial por proyecto, suite, caso y estado, además de un dashboard con indicadores.
La seguridad que la app ya trae por diseño
- Sesión con cookie HttpOnly, token firmado con HMAC-SHA256 y comparación en tiempo constante con secrets.compare_digest.
- Una política SQL aplicada en el backend: una sola sentencia por caso, DDL y TCL bloqueados y ROLLBACK obligatorio sobre todo DML.
- Reglas por ambiente: en TEST se admite DML con reversión; en STAGING además se exige confirmación explícita; en PRODUCTION solo se permite SELECT y las filas devueltas se ocultan.
Aun así, sabemos que ningún control es suficiente por sí solo. Un ROLLBACK no vuelve inocuo cualquier SQL (Oracle confirma de forma implícita al ejecutar DDL y admite transacciones autónomas), y sqlparse, la librería con la que reconocemos las sentencias, se describe a sí misma como un analizador, no como un validador. La seguridad tiene que ser por capas, y una de esas capas es mirar con lupa nuestro propio código.
Por qué integrar SAST en el desarrollo
SAST (Static Application Security Testing) analiza el código fuente sin ejecutarlo. Su gran ventaja es el momento: detecta patrones inseguros (credenciales escritas en el código, excepciones silenciadas, llamadas a procesos del sistema) en minutos y antes del despliegue, cuando corregir todavía es barato.
Elegimos Bandit porque está hecho específicamente para Python: recorre el árbol sintáctico (AST) de cada archivo, aplica reglas identificadas con códigos como B105 o B603, asocia cada hallazgo a un CWE y entrega un reporte en JSON fácil de procesar. OWASP lo incluye en su catálogo de herramientas de análisis de código fuente (aclarando que no avala ninguna en particular). Y conocemos su límite: Bandit encuentra patrones; no entiende la lógica de negocio ni revisa las dependencias.
2. Arquitectura del despliegue
Todo corre en una máquina virtual de Microsoft Azure con Docker Compose. Hacia afuera solo se exponen los puertos 80 y 443; la API y la base Oracle se comunican por la red interna de los contenedores.
Usuario ──HTTPS──▶ Caddy (80/443) ──▶ Contenedor API (FastAPI + frontend)
│ │
▼ ▼
SQLite Oracle Free (contenedor)
(metadatos e (base objetivo de
historial) la demostración)
| Componente | Rol |
|---|---|
| Caddy | Proxy inverso; termina HTTPS y enruta hacia la API |
| FastAPI | API REST, autenticación, política SQL y motor de ejecución; sirve también la interfaz web |
| SQLite + Alembic | Metadatos, casos, suites e historial, con migraciones versionadas |
| python-oracledb | Conexión a Oracle en modo Thin |
| Oracle Free | Base objetivo de la demostración |
| GitHub Actions | Escaneo SAST y despliegue automático |
- Aplicación en la nube: https://frameworksql.sytes.net/login
- Repositorio público: https://github.com/Geraldzvallos/framework-pruebas-sql-publico
Una aclaración honesta: Oracle Free sirve para la demostración, pero no cuenta con soporte ni parches del fabricante, así que en un entorno empresarial lo reemplazaríamos por una edición soportada.
El flujo de entrega queda así:
git push (main) ──▶ Job 1: SAST con Bandit ──¿pasa?──▶ Job 2: despliegue en la VM de Azure
│
└─ no ──▶ el pipeline se detiene y no se despliega
3. Implementación de la automatización
Paso 1: probar Bandit en local
pip install bandit
bandit -r . -f json -o bandit-report.json
-r recorre el repositorio de forma recursiva y -f json genera un reporte estructurado que luego podemos guardar como artefacto del pipeline o analizar con un script.
Paso 2: el workflow
Esta es una versión simplificada de nuestro workflow de GitHub Actions:
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
Las decisiones que más importan:
- Ejecución focalizada: En esta primera fase de CI/CD, priorizamos la ejecución de la herramienta SAST sobre nuestro directorio principal (./framework-pruebas-sql) usando el operador || true para asegurar que el pipeline no se rompa de inmediato y nos permita recoger los datos.
- Generación del artefacto: Usamos actions/upload-artifact@v4 para empaquetar el bandit-report.json. Esta es la decisión más útil, porque la plataforma guarda el reporte completo para que podamos descargarlo y analizar qué vulnerabilidades críticas existen antes de configurar el túnel SSH hacia la máquina de Azure.
Paso 3: el alcance importa
Nuestro primer escaneo apuntó a la raíz del repositorio (.), y eso incluyó la carpeta tests/. Como veremos en la siguiente sección, esa decisión multiplicó el ruido. Bandit permite fijar el alcance en pyproject.toml:
[tool.bandit]
exclude_dirs = ['tests']
pip install 'bandit[toml]'
bandit -c pyproject.toml -r .
4. Resultados del escaneo
El reporte de nuestro pipeline (generado el 3 de octubre de 2026 con Bandit 1.9.4) analizó 3 936 líneas de código:
| Métrica | Valor |
|---|---|
| Hallazgos totales | 467 |
| Severidad alta | 0 |
| Severidad media | 1 |
| Severidad baja | 466 |
| Confianza alta / media | 427 / 40 |
| Regla | Nombre | Cantidad | Severidad |
|---|---|---|---|
| B101 | assert_used | 410 | Baja |
| B105 | hardcoded_password_string | 25 | Baja |
| B106 | hardcoded_password_funcarg | 15 | Baja |
| B110 | try_except_pass | 5 | Baja |
| B603 | subprocess_without_shell_equals_true | 5 | Baja |
| B607 | start_process_with_partial_path | 4 | Baja |
| B404 | blacklist (import de subprocess) | 2 | Baja |
| B310 | blacklist (urllib.urlopen) | 1 | Media |
El dato que cambia la lectura
De los 467 hallazgos, 461 (98,7 %) están dentro de tests/. Solo 6 tocan otro código: 2 en app/engine/executor.py, 3 en una migración de Alembic y 1 en un script de validación. No hay ningún hallazgo de severidad alta.
Eso no significa que lo demás se ignore; significa que hay que priorizar. Estos son los hallazgos que pedíamos entender mejor.
B110: excepciones silenciadas (5 casos)
Bandit marcó cinco bloques try/except: pass, dos de ellos en el motor de ejecución, app/engine/executor.py:
try:
cursor.close()
except Exception:
pass
Por qué es un riesgo en producción: un except Exception: pass convierte un fallo en silencio. Si el cierre de un cursor o de una conexión falla de forma repetida, nadie lo sabrá hasta que se agoten las conexiones disponibles, y entonces el síntoma aparecerá lejos de la causa. En un motor cuya promesa es ejecutar sin dejar rastro, la trazabilidad de los errores es parte del producto. (Los otros tres casos están en la migración 001, alrededor de drop_constraint: ahí un fallo ignorado puede dejar el esquema a medio camino sin avisar.)
Cómo lo corregiríamos: capturar la excepción específica del driver y dejar constancia.
import logging
logger = logging.getLogger(_name_)
try:
cursor.close()
except oracledb.Error:
logger.warning('No se pudo cerrar el cursor', exc_info=True)
B310: urlopen sin validar el esquema (1 caso, severidad media)
Es el único hallazgo de severidad media, y está en scripts/validar_flujo_oracle.py, que llama a la API con urllib.request.urlopen(req).
Por qué es un riesgo: urlopen acepta esquemas como file://. Si la URL llegara a ser controlable por un tercero, podría leerse un archivo local en lugar de hacer una petición web (CWE-22). En nuestro caso es un script interno, así que el riesgo real es bajo, pero la corrección cuesta dos líneas:
from urllib.parse import urlparse
if urlparse(url).scheme not in {'http', 'https'}:
raise ValueError('Solo se permiten URLs http o https')
B105 y B106: contraseñas escritas en el código (40 casos)
Son los que más asustan. Bandit encontró 25 cadenas y 15 argumentos con aspecto de contraseña, por ejemplo:
executor = TargetDatabaseExecutor(dsn='localhost/xe', user='user', password='secret_password')
Por qué es un riesgo en producción: una credencial real escrita en el código termina en el historial de Git para siempre, y en un repositorio público queda expuesta a cualquiera (CWE-259). Es una de las fallas más comunes y más fáciles de explotar.
Pero aquí el contexto manda: los 40 casos están en archivos de pruebas y son datos ficticios (secret_password, super_secret_password_999). Bandit no puede distinguir una credencial real de un valor de ejemplo; esa es la razón por la que siempre hay que revisar a mano. Lo correcto es declarar esos valores como constantes o fixtures y, cuando sean ficticios, documentarlo con # nosec B105 y un comentario que explique por qué.
B603, B607 y B404: procesos del sistema (11 casos)
Los tests lanzan Alembic mediante subprocess:
subprocess.run(['alembic', 'upgrade', 'head'], env=env, check=True)
Por qué es un riesgo en producción: B603 invita a verificar que ninguna entrada no confiable llegue al comando (inyección de comandos, CWE-78); B607 advierte que se invoca un ejecutable solo por nombre, de modo que, si alguien manipula el PATH, podría ejecutarse otro programa. En nuestro caso los argumentos son constantes y no se usa shell=True, de modo que el riesgo es bajo, y el código vive en tests/. Aun así, una mejora simple es fijar el intérprete:
import sys
subprocess.run([sys.executable, '-m', 'alembic', 'upgrade', 'head'], env=env, check=True)
Conviene, además, verificar que tests/ no se copie a la imagen de producción (.dockerignore).
B101: los 410 assert (el ruido)
Bandit avisa de que los assert desaparecen al ejecutar Python con -O, y eso sería grave si protegieran una regla de seguridad. En tests/ es el mecanismo normal de pytest. Son el 88 % del reporte y no aportan información accionable.
Resumen de priorización
| Prioridad | Hallazgo | Acción |
|---|---|---|
| 1 | B110 en executor.py | Registrar el error y capturar oracledb.Error |
| 2 | B310 en el script de validación | Validar el esquema de la URL |
| 3 | B110 en la migración | Revisar si el fallo ignorado es intencional |
| 4 | B105/B106 en tests | Constantes o fixtures; # nosec justificado |
| 5 | B603/B607/B404 en tests | Usar sys.executable |
| — | B101 en tests | Excluir del escaneo |
5. Conclusión y aprendizajes
- Automatizar el escaneo cambia la conversación. Con Bandit en cada push la seguridad dejó de ser una revisión final y pasó a ser parte del flujo normal.
- El número de hallazgos no es el riesgo. 467 hallazgos y cero de severidad alta es un resultado muy distinto de lo que sugiere el titular. Leer la severidad, la confianza y, sobre todo, la ubicación es lo que convierte un reporte en decisiones.
- Bloquear por severidad, informar por todo lo demás. Una puerta que falla por cualquier hallazgo bajo se termina desactivando; una que falla solo por severidad alta se respeta.
- Definir bien el alcance. Escanear tests/ sin ajustes llenó el reporte de ruido. La alternativa razonable es que la puerta de seguridad revise el código de producción y que los tests se escaneen solo de forma informativa.
- SAST es una capa, no la defensa. Bandit no revisa dependencias, no ve la lógica de negocio de nuestra política SQL y no sustituye pruebas negativas ni permisos mínimos en Oracle.
Próximos pasos: corregir los B110 y el B310, auditar dependencias (por ejemplo, con pip-audit), fijar el alcance de Bandit en pyproject.toml y publicar los resultados en la pestaña de seguridad de GitHub.
6. Video demostrativo
Mostramos el pipeline en acción, desde el push hasta el despliegue automático:
- Aplicación: https://frameworksql.sytes.net/login
- Repositorio: https://github.com/Geraldzvallos/framework-pruebas-sql-publico
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.