DEV Community

GERALD DEYBI ZEVALLOS PINTO
GERALD DEYBI ZEVALLOS PINTO

Posted on

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

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

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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:

Top comments (0)