DEV Community

467 hallazgos de Bandit: análisis de seguridad con GitHub Actions y despliegue en Azure

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:

  1. Código, en GitHub. El repositorio es público: https://github.com/Geraldzvallos/framework-pruebas-sql-publico
  2. Automatización, en GitHub Actions. Cada push a la rama main ejecuta el escáner de seguridad y, solo si pasa, actualiza el despliegue.
  3. 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 │
└──────────┘           └──────────────────────────────┘          └───────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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']
Enter fullscreen mode Exit fullscreen mode
pip install 'bandit[toml]'
bandit -c pyproject.toml -r .
Enter fullscreen mode Exit fullscreen mode

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

  1. Severidad. Ninguno es alto. Ya con eso se desinfla el susto inicial.
  2. Ubicación. De los 467 hallazgos, 461 (98,7 %) están en tests/. Solo 6 afectan a código que no es de pruebas.
  3. 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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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

  1. 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.
  2. 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.
  3. Una puerta que bloquea todo se desactiva. Bloquear por severidad alta e informar el resto mantuvo el pipeline útil y respetado.
  4. 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.
  5. 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:

https://youtu.be/hDyOvQJL2pE

Top comments (0)