<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: GERALD DEYBI ZEVALLOS PINTO</title>
    <description>The latest articles on DEV Community by GERALD DEYBI ZEVALLOS PINTO (@gerald_deybizevallospin).</description>
    <link>https://dev.to/gerald_deybizevallospin</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4160798%2Fd2af4b88-fdcb-4d1e-ac73-6d48ad8e5156.jpg</url>
      <title>DEV Community: GERALD DEYBI ZEVALLOS PINTO</title>
      <link>https://dev.to/gerald_deybizevallospin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gerald_deybizevallospin"/>
    <language>en</language>
    <item>
      <title>Backups en la nube sin dramas</title>
      <dc:creator>GERALD DEYBI ZEVALLOS PINTO</dc:creator>
      <pubDate>Sun, 04 Oct 2026 04:24:33 +0000</pubDate>
      <link>https://dev.to/gerald_deybizevallospin/backups-en-la-nube-sin-dramas-56ap</link>
      <guid>https://dev.to/gerald_deybizevallospin/backups-en-la-nube-sin-dramas-56ap</guid>
      <description>&lt;p&gt;&lt;em&gt;Cómo protegemos SQLite y Oracle en un despliegue automatizado en Azure&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  1. Introducción: backups, RPO, RTO y qué motores usamos
&lt;/h1&gt;

&lt;p&gt;Un backup que nunca se restauró es una esperanza, no un plan. Antes de tocar un solo comando, conviene fijar dos números:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RPO (Recovery Point Objective):&lt;/strong&gt; cuántos datos estamos dispuestos a perder, medido en tiempo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RTO (Recovery Time Objective):&lt;/strong&gt; cuánto tiempo podemos tardar en volver a operar.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nuestro caso de estudio es un &lt;strong&gt;Framework de Pruebas SQL&lt;/strong&gt; hecho con FastAPI. Permite definir casos de prueba, agruparlos en suites, ejecutarlos contra Oracle y guardar un historial de resultados (PASS, FAIL o ERROR). Para dejar claro el alcance: &lt;strong&gt;trabajamos con SQLite y Oracle Free, y no usamos MS SQL Server&lt;/strong&gt;. Este artículo no lo cubre.&lt;/p&gt;

&lt;p&gt;SQLite guarda los metadatos de la aplicación: proyectos, perfiles de conexión, casos, suites e historial. Oracle es la base &lt;em&gt;objetivo&lt;/em&gt; donde corren las pruebas. Esa diferencia de roles define toda nuestra estrategia.&lt;/p&gt;

&lt;h1&gt;
  
  
  2. Arquitectura: Azure, contenedores y volúmenes
&lt;/h1&gt;

&lt;p&gt;Todo corre en una máquina virtual de Azure con Docker Compose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Caddy&lt;/strong&gt; recibe el tráfico y termina HTTPS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contenedor de la API&lt;/strong&gt; (framework_api_prod): FastAPI más el frontend. Escribe en SQLite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contenedor de Oracle Free&lt;/strong&gt; (framework_oracle_prod): base objetivo de la demostración.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Volúmenes persistentes&lt;/strong&gt; para SQLite y para oracle_data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Los contenedores son desechables y los volúmenes no. El código y las imágenes se reconstruyen en minutos, pero el historial de ejecuciones solo existe en el volumen. Por eso &lt;strong&gt;el volumen es el activo que hay que proteger&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Hay otra decisión de diseño que ayuda. Nuestros perfiles de conexión &lt;strong&gt;nunca almacenan la contraseña de Oracle&lt;/strong&gt;: se pide en el momento de probar o ejecutar. Así, un backup de SQLite no filtra credenciales de bases externas. Sí contiene texto SQL y resultados esperados, así que lo tratamos como información potencialmente sensible y lo cifraríamos en reposo.&lt;/p&gt;

&lt;h1&gt;
  
  
  3. Backup de SQLite: frío y en caliente
&lt;/h1&gt;

&lt;h3&gt;
  
  
  Frío (con la aplicación detenida)
&lt;/h3&gt;

&lt;p&gt;Es el más simple y el más seguro:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash
docker compose stop api
&lt;span class="nb"&gt;cp&lt;/span&gt; /ruta/volumen/framework.db /backups/framework_&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%Y%m%dT%H%M%SZ&lt;span class="si"&gt;)&lt;/span&gt;.db
docker compose start api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El costo es una breve interrupción. Es aceptable en una ventana de mantenimiento, no como rutina diaria.&lt;/p&gt;

&lt;h3&gt;
  
  
  En caliente (con la aplicación en uso)
&lt;/h3&gt;

&lt;p&gt;Un cp sobre una base activa puede producir una copia inconsistente. Si hay modo WAL, copiar solo el .db deja fuera transacciones recientes. Lo correcto es usar la &lt;strong&gt;API de backup de SQLite&lt;/strong&gt;, que genera una copia coherente mientras la app sigue escribiendo. Usamos Python porque ya está en la imagen de la API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; framework_api_prod python - &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;PY&lt;/span&gt;&lt;span class="sh"&gt;'
import sqlite3
src = sqlite3.connect("/app/data/framework.db")      # ruta ilustrativa
dst = sqlite3.connect("/app/data/backups/snapshot.db")
with dst:
    src.backup(dst)
dst.close(); src.close()
&lt;/span&gt;&lt;span class="no"&gt;PY
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Después verificamos y comprimimos la copia antes de subirla:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash
sqlite3 snapshot.db &lt;span class="s2"&gt;"PRAGMA integrity_check;"&lt;/span&gt;   &lt;span class="c"&gt;# debe responder: ok&lt;/span&gt;
&lt;span class="nb"&gt;gzip&lt;/span&gt; &lt;span class="nt"&gt;-9&lt;/span&gt; snapshot.db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Fuera de la VM
&lt;/h3&gt;

&lt;p&gt;Una copia en el mismo disco no sirve si se pierde la VM. La subimos a un almacenamiento separado, por ejemplo Azure Blob Storage con azcopy o az storage blob upload. Un cron o un timer de systemd la programa según el RPO acordado.&lt;/p&gt;

&lt;p&gt;Un detalle de honestidad técnica: los snapshots de disco de la VM suelen ser consistentes ante caída, no coherentes a nivel de aplicación. Son un complemento, no un reemplazo del backup lógico.&lt;/p&gt;

&lt;h1&gt;
  
  
  4. Backup lógico del contenedor de Oracle
&lt;/h1&gt;

&lt;p&gt;Aquí hay un matiz importante. En nuestro despliegue, Oracle Free contiene un esquema de pruebas que se crea con un script de inicialización (001_test_schema.sh). En parte es &lt;strong&gt;reproducible&lt;/strong&gt;, pero si los casos de prueba dependen de datos cargados después, necesitamos un backup lógico con &lt;strong&gt;Data Pump&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash
docker &lt;span class="nb"&gt;exec &lt;/span&gt;framework_oracle_prod bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'
  expdp userid="system@//localhost:1521/FREEPDB1" \
        schemas=ESQUEMA_PRUEBAS \
        directory=DATA_PUMP_DIR \
        dumpfile=pruebas_%U.dmp logfile=pruebas_exp.log'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nombres de esquema y credenciales son ilustrativos. En la práctica usaríamos un parfile con permisos restringidos para no exponer contraseñas en la línea de comandos. El .dmp se copia fuera del contenedor y luego a almacenamiento externo, igual que SQLite. Como alternativa en frío, se detiene el contenedor y se empaqueta el volumen oracle_data.&lt;/p&gt;

&lt;p&gt;Una advertencia: &lt;strong&gt;Oracle Free no recibe soporte ni parches del fabricante&lt;/strong&gt;. Para producción real se usa una edición soportada, y la estrategia de backup se rediseña con las herramientas que ese licenciamiento permita.&lt;/p&gt;

&lt;h1&gt;
  
  
  5. Cómo la automatización acelera la recuperación
&lt;/h1&gt;

&lt;p&gt;Nuestro despliegue se actualiza con GitHub Actions. Como el código y la configuración viven en el repositorio, ante un desastre &lt;strong&gt;no hay que reconstruir a mano la aplicación&lt;/strong&gt;. La receta es: nueva VM (o la misma), pipeline de despliegue y restaurar el volumen de datos.&lt;/p&gt;

&lt;p&gt;Un punto fuerte de integrar backups con CI/CD es &lt;strong&gt;respaldar antes de cada despliegue&lt;/strong&gt;, porque ahí se ejecutan las migraciones Alembic sobre SQLite, el momento más riesgoso:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;yaml&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Backup previo al despliegue&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ssh deploy@$VM "/opt/framework/scripts/backup_sqlite.sh"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Desplegar&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ssh deploy@$VM "cd /opt/framework &amp;amp;&amp;amp; docker compose pull &amp;amp;&amp;amp; docker compose up -d"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Este flujo es ilustrativo. Cada equipo lo adapta a sus secretos y rutas.&lt;/p&gt;

&lt;h1&gt;
  
  
  6. Conclusión
&lt;/h1&gt;

&lt;p&gt;Dos ideas resumen lo aprendido. Primero, &lt;strong&gt;separar lo reproducible de lo irremplazable&lt;/strong&gt;: el código se redespliega, los datos se restauran. Segundo, &lt;strong&gt;un backup vale lo que vale su restauración probada&lt;/strong&gt;. Nuestro siguiente paso es automatizar la copia consistente y ensayar restauraciones periódicas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🎥 Mira la demostración aquí:&lt;/strong&gt;   &lt;iframe src="https://www.youtube.com/embed/JM4jHQJt3Mg" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;💻 Repositorio:&lt;/strong&gt; &lt;a href="https://github.com/Geraldzvallos/framework-pruebas-sql-publico" rel="noopener noreferrer"&gt;https://github.com/Geraldzvallos/framework-pruebas-sql-publico&lt;/a&gt;&lt;/p&gt;

</description>
      <category>azure</category>
      <category>cloud</category>
      <category>database</category>
      <category>devops</category>
    </item>
    <item>
      <title>De git push a producción sin sorpresas: SAST con Bandit en un pipeline de GitHub Actions</title>
      <dc:creator>GERALD DEYBI ZEVALLOS PINTO</dc:creator>
      <pubDate>Sun, 04 Oct 2026 03:49:07 +0000</pubDate>
      <link>https://dev.to/gerald_deybizevallospin/de-git-push-a-produccion-sin-sorpresas-sast-con-bandit-en-un-pipeline-de-github-actions-1k69</link>
      <guid>https://dev.to/gerald_deybizevallospin/de-git-push-a-produccion-sin-sorpresas-sast-con-bandit-en-un-pipeline-de-github-actions-1k69</guid>
      <description>&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Introducción
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Qué construimos
&lt;/h3&gt;

&lt;p&gt;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 &lt;strong&gt;Framework de Pruebas de Base de Datos SQL&lt;/strong&gt;: 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Hoy permite:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gestionar proyectos y perfiles de conexión&lt;/strong&gt; a Oracle. Un perfil guarda host, puerto, usuario y service name, pero &lt;strong&gt;nunca la contraseña&lt;/strong&gt;: se solicita solo al probar la conexión o ejecutar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Definir casos de prueba&lt;/strong&gt; con dos validaciones: ROW_COUNT (número de filas esperado) y EXISTS (una condición verdadera o falsa).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agrupar casos en suites&lt;/strong&gt; del mismo proyecto.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ejecutar y clasificar&lt;/strong&gt; cada resultado como PASS, FAIL o ERROR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consultar el historial&lt;/strong&gt; por proyecto, suite, caso y estado, además de un dashboard con indicadores.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  La seguridad que la app ya trae por diseño
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Sesión con cookie HttpOnly, token firmado con HMAC-SHA256 y comparación en tiempo constante con secrets.compare_digest.&lt;/li&gt;
&lt;li&gt;Una política SQL aplicada en el backend: una sola sentencia por caso, DDL y TCL bloqueados y ROLLBACK obligatorio sobre todo DML.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Por qué integrar SAST en el desarrollo
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;SAST&lt;/strong&gt; (&lt;em&gt;Static Application Security Testing&lt;/em&gt;) 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.&lt;/p&gt;

&lt;p&gt;Elegimos &lt;strong&gt;Bandit&lt;/strong&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Arquitectura del despliegue
&lt;/h2&gt;

&lt;p&gt;Todo corre en una &lt;strong&gt;máquina virtual de Microsoft Azure&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;Usuario ──HTTPS──▶ Caddy (80/443) ──▶ Contenedor API (FastAPI + frontend)&lt;br&gt;&lt;br&gt;
│ │&lt;br&gt;&lt;br&gt;
▼ ▼&lt;br&gt;&lt;br&gt;
SQLite Oracle Free (contenedor)&lt;br&gt;&lt;br&gt;
(metadatos e (base objetivo de&lt;br&gt;&lt;br&gt;
historial) la demostración)&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Componente&lt;/th&gt;
&lt;th&gt;Rol&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Caddy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Proxy inverso; termina HTTPS y enruta hacia la API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FastAPI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;API REST, autenticación, política SQL y motor de ejecución; sirve también la interfaz web&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SQLite + Alembic&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Metadatos, casos, suites e historial, con migraciones versionadas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;python-oracledb&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Conexión a Oracle en modo Thin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Oracle Free&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Base objetivo de la demostración&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GitHub Actions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Escaneo SAST y despliegue automático&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt; &lt;strong&gt;Aplicación en la nube:&lt;/strong&gt; &lt;a href="https://frameworksql.sytes.net/login" rel="noopener noreferrer"&gt;https://frameworksql.sytes.net/login&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Repositorio público:&lt;/strong&gt; &lt;a href="https://github.com/Geraldzvallos/framework-pruebas-sql-publico" rel="noopener noreferrer"&gt;https://github.com/Geraldzvallos/framework-pruebas-sql-publico&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;El flujo de entrega queda así:&lt;/p&gt;

&lt;p&gt;git push (main) ──▶ Job 1: SAST con Bandit ──¿pasa?──▶ Job 2: despliegue en la VM de Azure&lt;br&gt;&lt;br&gt;
│&lt;br&gt;&lt;br&gt;
└─ no ──▶ el pipeline se detiene y no se despliega&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Implementación de la automatización
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Paso 1: probar Bandit en local
&lt;/h3&gt;

&lt;p&gt;pip install bandit&lt;br&gt;&lt;br&gt;
bandit -r . -f json -o bandit-report.json&lt;/p&gt;

&lt;p&gt;-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.&lt;/p&gt;
&lt;h3&gt;
  
  
  Paso 2: el workflow
&lt;/h3&gt;

&lt;p&gt;Esta es una versión simplificada de nuestro workflow de GitHub Actions:&lt;/p&gt;

&lt;p&gt;name: Security Scan and Deploy&lt;/p&gt;

&lt;p&gt;on:&lt;/p&gt;

&lt;p&gt;push:&lt;/p&gt;

&lt;p&gt;branches:&lt;/p&gt;

&lt;p&gt;- main&lt;/p&gt;

&lt;p&gt;jobs:&lt;/p&gt;

&lt;p&gt;security-scan:&lt;/p&gt;

&lt;p&gt;runs-on: ubuntu-latest&lt;/p&gt;

&lt;p&gt;steps:&lt;/p&gt;

&lt;p&gt;- name: Checkout code&lt;/p&gt;

&lt;p&gt;uses: actions/checkout@v4&lt;/p&gt;

&lt;p&gt;- name: Set up Python&lt;/p&gt;

&lt;p&gt;uses: actions/setup-python@v5&lt;/p&gt;

&lt;p&gt;with:&lt;/p&gt;

&lt;p&gt;python-version: '3.12'&lt;/p&gt;

&lt;p&gt;- name: Install Bandit (SAST Tool)&lt;/p&gt;

&lt;p&gt;run: pip install bandit&lt;/p&gt;

&lt;p&gt;- name: Run Security Scan&lt;/p&gt;

&lt;p&gt;run: bandit -r ./framework-pruebas-sql -f json -o bandit-report.json || true&lt;/p&gt;

&lt;p&gt;- name: Upload Security Report&lt;/p&gt;

&lt;p&gt;uses: actions/upload-artifact@v4&lt;/p&gt;

&lt;p&gt;with:&lt;/p&gt;

&lt;p&gt;name: sast-report&lt;/p&gt;

&lt;p&gt;path: bandit-report.json&lt;/p&gt;

&lt;p&gt;Las decisiones que más importan:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Paso 3: el alcance importa
&lt;/h3&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;p&gt;[tool.bandit]&lt;br&gt;&lt;br&gt;
exclude_dirs = ['tests']&lt;/p&gt;

&lt;p&gt;pip install 'bandit[toml]'&lt;br&gt;&lt;br&gt;
bandit -c pyproject.toml -r .&lt;/p&gt;
&lt;h2&gt;
  
  
  4. Resultados del escaneo
&lt;/h2&gt;

&lt;p&gt;El reporte de nuestro pipeline (generado el 3 de octubre de 2026 con Bandit 1.9.4) analizó &lt;strong&gt;3 936 líneas de código&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Métrica&lt;/th&gt;
&lt;th&gt;Valor&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hallazgos totales&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;467&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Severidad alta&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Severidad media&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Severidad baja&lt;/td&gt;
&lt;td&gt;466&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Confianza alta / media&lt;/td&gt;
&lt;td&gt;427 / 40&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Regla&lt;/th&gt;
&lt;th&gt;Nombre&lt;/th&gt;
&lt;th&gt;Cantidad&lt;/th&gt;
&lt;th&gt;Severidad&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;B101&lt;/td&gt;
&lt;td&gt;assert_used&lt;/td&gt;
&lt;td&gt;410&lt;/td&gt;
&lt;td&gt;Baja&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B105&lt;/td&gt;
&lt;td&gt;hardcoded_password_string&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;Baja&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B106&lt;/td&gt;
&lt;td&gt;hardcoded_password_funcarg&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;Baja&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B110&lt;/td&gt;
&lt;td&gt;try_except_pass&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Baja&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B603&lt;/td&gt;
&lt;td&gt;subprocess_without_shell_equals_true&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Baja&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B607&lt;/td&gt;
&lt;td&gt;start_process_with_partial_path&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Baja&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B404&lt;/td&gt;
&lt;td&gt;blacklist (import de subprocess)&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Baja&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B310&lt;/td&gt;
&lt;td&gt;blacklist (urllib.urlopen)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Media&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  El dato que cambia la lectura
&lt;/h3&gt;

&lt;p&gt;De los 467 hallazgos, &lt;strong&gt;461 (98,7 %) están dentro de tests/&lt;/strong&gt;. 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.&lt;/p&gt;

&lt;p&gt;Eso no significa que lo demás se ignore; significa que hay que &lt;strong&gt;priorizar&lt;/strong&gt;. Estos son los hallazgos que pedíamos entender mejor.&lt;/p&gt;
&lt;h3&gt;
  
  
  B110: excepciones silenciadas (5 casos)
&lt;/h3&gt;

&lt;p&gt;Bandit marcó cinco bloques try/except: pass, dos de ellos en el motor de ejecución, app/engine/executor.py:&lt;/p&gt;

&lt;p&gt;try:&lt;br&gt;&lt;br&gt;
cursor.close()&lt;br&gt;&lt;br&gt;
except Exception:&lt;br&gt;&lt;br&gt;
pass&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Por qué es un riesgo en producción:&lt;/strong&gt; 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.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cómo lo corregiríamos:&lt;/strong&gt; capturar la excepción específica del driver y dejar constancia.&lt;/p&gt;

&lt;p&gt;import logging&lt;br&gt;&lt;br&gt;
logger = logging.getLogger(_&lt;em&gt;name&lt;/em&gt;_)&lt;br&gt;&lt;br&gt;
&lt;br&gt;try:&lt;br&gt;&lt;br&gt;
cursor.close()&lt;br&gt;&lt;br&gt;
except oracledb.Error:&lt;br&gt;&lt;br&gt;
logger.warning('No se pudo cerrar el cursor', exc_info=True)&lt;/p&gt;
&lt;h3&gt;
  
  
  B310: urlopen sin validar el esquema (1 caso, severidad media)
&lt;/h3&gt;

&lt;p&gt;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).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Por qué es un riesgo:&lt;/strong&gt; 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:&lt;/p&gt;

&lt;p&gt;from urllib.parse import urlparse&lt;br&gt;&lt;br&gt;
&lt;br&gt;if urlparse(url).scheme not in {'http', 'https'}:&lt;br&gt;&lt;br&gt;
raise ValueError('Solo se permiten URLs http o https')&lt;/p&gt;
&lt;h3&gt;
  
  
  B105 y B106: contraseñas escritas en el código (40 casos)
&lt;/h3&gt;

&lt;p&gt;Son los que más asustan. Bandit encontró 25 cadenas y 15 argumentos con aspecto de contraseña, por ejemplo:&lt;/p&gt;

&lt;p&gt;executor = TargetDatabaseExecutor(dsn='localhost/xe', user='user', password='secret_password')&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Por qué es un riesgo en producción:&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pero aquí el contexto manda:&lt;/strong&gt; 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 &lt;em&gt;fixtures&lt;/em&gt; y, cuando sean ficticios, documentarlo con # nosec B105 y un comentario que explique por qué.&lt;/p&gt;
&lt;h3&gt;
  
  
  B603, B607 y B404: procesos del sistema (11 casos)
&lt;/h3&gt;

&lt;p&gt;Los tests lanzan Alembic mediante subprocess:&lt;/p&gt;

&lt;p&gt;subprocess.run(['alembic', 'upgrade', 'head'], env=env, check=True)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Por qué es un riesgo en producción:&lt;/strong&gt; 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:&lt;/p&gt;

&lt;p&gt;import sys&lt;br&gt;&lt;br&gt;
subprocess.run([sys.executable, '-m', 'alembic', 'upgrade', 'head'], env=env, check=True)&lt;/p&gt;

&lt;p&gt;Conviene, además, verificar que tests/ no se copie a la imagen de producción (.dockerignore).&lt;/p&gt;
&lt;h3&gt;
  
  
  B101: los 410 assert (el ruido)
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;
&lt;h3&gt;
  
  
  Resumen de priorización
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prioridad&lt;/th&gt;
&lt;th&gt;Hallazgo&lt;/th&gt;
&lt;th&gt;Acción&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;B110 en executor.py&lt;/td&gt;
&lt;td&gt;Registrar el error y capturar oracledb.Error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;B310 en el script de validación&lt;/td&gt;
&lt;td&gt;Validar el esquema de la URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;B110 en la migración&lt;/td&gt;
&lt;td&gt;Revisar si el fallo ignorado es intencional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;B105/B106 en tests&lt;/td&gt;
&lt;td&gt;Constantes o fixtures; # nosec justificado&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;B603/B607/B404 en tests&lt;/td&gt;
&lt;td&gt;Usar sys.executable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;B101 en tests&lt;/td&gt;
&lt;td&gt;Excluir del escaneo&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  5. Conclusión y aprendizajes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Automatizar el escaneo cambia la conversación.&lt;/strong&gt; Con Bandit en cada push la seguridad dejó de ser una revisión final y pasó a ser parte del flujo normal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El número de hallazgos no es el riesgo.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bloquear por severidad, informar por todo lo demás.&lt;/strong&gt; Una puerta que falla por cualquier hallazgo bajo se termina desactivando; una que falla solo por severidad alta se respeta.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Definir bien el alcance.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SAST es una capa, no la defensa.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Próximos pasos:&lt;/strong&gt; 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.&lt;/p&gt;
&lt;h2&gt;
  
  
  6. Video demostrativo
&lt;/h2&gt;

&lt;p&gt;Mostramos el pipeline en acción, desde el push hasta el despliegue automático:   &lt;iframe src="https://www.youtube.com/embed/hDyOvQJL2pE" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Aplicación: &lt;a href="https://frameworksql.sytes.net/login" rel="noopener noreferrer"&gt;https://frameworksql.sytes.net/login&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repositorio: &lt;a href="https://github.com/Geraldzvallos/framework-pruebas-sql-publico" rel="noopener noreferrer"&gt;https://github.com/Geraldzvallos/framework-pruebas-sql-publico&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cicd</category>
      <category>github</category>
      <category>python</category>
      <category>security</category>
    </item>
  </channel>
</rss>
