Saltar al contenido principal

Capítulo III: Desarrollo de la Solución y Desafíos Técnicos

Este capítulo detalla la justificación del stack tecnológico adoptado, la metodología de desarrollo de la solución y un análisis exhaustivo de los principales desafíos de ingeniería a los que nos enfrentamos y cómo los resolvimos de manera óptima.


🛠️ Justificación del Stack Tecnológico

La elección de las tecnologías para construir el ecosistema TVAPyMM se basó en criterios de rendimiento, mantenibilidad, velocidad de respuesta y costo operativo.

1. Backend: Kotlin & Spring Boot 3.5

  • Kotlin: Aporta total interoperabilidad con el ecosistema de Java pero con una sintaxis moderna, eliminación de excepciones de referencia nula (Null Safety) en tiempo de compilación y, fundamentalmente, soporte nativo de primera clase para Coroutines.
  • Spring Boot: Provee un marco empresarial maduro con una inyección de dependencias sumamente potente, gestión transaccional sólida con Spring Data JPA y una integración robusta con Spring Security.

2. Frontend: React, TypeScript & Vite

  • React: Permite crear una interfaz basada en componentes altamente reactiva, idónea para construir un dashboard interactivo de proyectos y reportes de vulnerabilidades.
  • TypeScript: Garantiza la seguridad de tipos en el cliente, evitando fallos de mapeo de datos comunes al consumir los payloads JSON de la API.
  • Vite: Reemplaza a las herramientas de compilación clásicas lentas como Webpack, acelerando drásticamente el flujo de desarrollo local (Hot Module Replacement en milisegundos).

💥 Desafíos Técnicos y Resoluciones de Ingeniería

Durante las diferentes fases del ciclo de vida del proyecto, nos enfrentamos a complejos desafíos de ingeniería de software. A continuación se detallan los cuatro problemas principales y el diseño técnico empleado para solucionarlos:


Desafío 1: Concurrencia Seguro y Agregación de Resultados Sin Bloqueos

El Problema

Al despachar un escaneo que involucra múltiples categorías de seguridad (ej. inyecciones, fallos de TLS, cookies desprotegidas), ejecutar los escáneres uno por uno (de forma síncrona) disparaba el tiempo de respuesta total del servidor a niveles inaceptables para el cliente web. Sin embargo, utilizar hilos clásicos de Java (Thread) para cada escáner requería una gran cantidad de memoria RAM y un complejo cableado de sincronización de semáforos para recolectar y agregar la lista de vulnerabilidades encontradas sin sufrir condiciones de carrera (Race Conditions).

La Solución

Implementamos Kotlin Coroutines aprovechando el contexto asíncrono no bloqueante. El método principal de orquestación lanza cada escáner dentro de un bloque async y luego suspende su ejecución con awaitAll().

Para garantizar la inmutabilidad y evitar condiciones de carrera en la agregación de resultados, el flujo no utiliza colecciones compartidas mutables entre hilos. En su lugar, cada corrutina retorna su propia lista de vulnerabilidades inmutable (List<Vulnerability>), y al finalizar la espera concurrente, las listas se consolidan mediante un mapeo funcional plano flatten() en un único hilo seguro coordinado por el despachador de Kotlin:

// Agregación funcional de reportes segura ante hilos
val findings: List<Vulnerability> = scanners
.filter { it.supports(categories) }
.map { scanner -> async { scanner.scan(url, project) } }
.awaitAll()
.flatten() // Combina de forma segura y sin bloqueos de sincronización

Desafío 2: Mitigación de Denegaciones de Servicio (DoS) en el Objetivo

El Problema

El motor asíncrono es tan veloz que, al ejecutar decenas de comprobaciones concurrentes de red y peticiones HTTP GET/POST contra la aplicación objetivo, existía el riesgo de:

  1. Saturar la capacidad del servidor de la aplicación de prueba o del cliente auditado.
  2. Desencadenar alarmas automáticas de bloqueo en cortafuegos de aplicaciones web (WAF) o sistemas de prevención de intrusos (IPS) del host destino, arruinando la legitimidad del análisis.

La Solución

Implementamos un mecanismo de control de flujo doble:

  1. Límite de Escaneos Concurrentes a Nivel Global: Mediante la propiedad app.scan.max-concurrent-scans=2 en application.properties, limitamos la cantidad máxima de proyectos que pueden analizarse de manera concurrente en todo el backend.
  2. Límites Internos de Frecuencia (Rate Limiting) en Escáneres: Los plugins que realizan peticiones repetitivas de payloads (ej. inyecciones SQL) introducen sutiles retardos no bloqueantes utilizando la función nativa delay(ms) de Kotlin, la cual libera el hilo del servidor web para realizar otras tareas mientras espera, en lugar de bloquear los recursos de procesamiento.

Desafío 3: Aprovisionamiento de Usuarios Stateless con Auth0

El Problema

Queríamos evitar a toda costa la necesidad de pantallas de registro tediosas o formularios manuales de perfiles de usuario. Sin embargo, al operar bajo una arquitectura REST puramente stateless mediante JWT de Auth0, el backend no almacena sesiones tradicionales de base de datos. Necesitábamos asociar proyectos y vulnerabilidades a un ID interno del usuario local pero sin comprometer la velocidad ni bloquear las transacciones de lectura/escritura en PostgreSQL.

La Solución

Diseñamos un Flujo de Auto-aprovisionamiento Transaccional en el endpoint GET /users/me. Cuando el frontend de React se monta tras el login de Auth0, invoca este endpoint adjuntando el JWT. El backend valida de forma stateless la firma digital y el tiempo de expiración contra los servidores seguros de Auth0 de manera asíncrona. Si el JWT es válido, extrae el identificador único auth0Id (ej. auth0|65fbc0...) y ejecuta una transacción aislada de base de datos:

  • Busca el registro en PostgreSQL.
  • Si no existe, realiza un registro "al vuelo" (on-the-fly) asignándole un UUID interno persistente y asociándole sus datos de email y nombre contenidos en el JWT.
  • Retorna el DTO de usuario unificado.

De esta manera, la base de datos se auto-hidrata dinámicamente en la primera llamada de cada sesión de usuario sin interrumpir el flujo del usuario con pasos adicionales de registro manual.


Desafío 4: La Gran Migración: De AWS S3 a Oracle Cloud (OCI) Always Free

El Problema

Inicialmente, la plataforma utilizaba AWS S3 para almacenar de forma segura los reportes Markdown de las auditorías de seguridad, y servidores de desarrollo locales para el backend. No obstante, al expirar el año de capa gratuita (Free Tier) de AWS, los costos de almacenamiento y procesamiento comenzaron a cobrarse mensualmente, un escenario insostenible para un proyecto académico de tesis con presupuesto cero.

Necesitábamos migrar por completo la infraestructura de la plataforma a la nube pero manteniendo el costo en $0.00 USD, sin perder rendimiento y sin reescribir por completo la lógica de negocio y las llamadas del servicio de subida de reportes del backend.

La Solución

Diseñamos una migración integral hacia Oracle Cloud Infrastructure (OCI) aprovechando su generosa capa Always Free (Siempre Gratis). La solución técnica abarcó tres frentes:

  1. Migración del Servidor y Base de Datos: Desplegamos una instancia de cómputo en Oracle Cloud con arquitectura ARM (Ampere) de 4 cores y 24 GB de RAM, junto con un contenedor PostgreSQL optimizado en rendimiento que absorbió la base de datos local y de pruebas sin ningún costo.
  2. Abstracción del Servicio de Almacenamiento (API Compatible con S3): Para evitar modificar la lógica de firmas del SDK de S3 implementada en Kotlin, configuramos el bucket en Oracle Object Storage en modo de compatibilidad con la API de Amazon S3 (Amazon S3 Compatibility API). Esto nos permitió conservar intacto el código de S3Service.kt y del SDK v2 de AWS, variando únicamente el endpoint del destino mediante variables de entorno.
  3. Mapeo Dinámico de Proveedores de Almacenamiento: Introdujimos un interruptor dinámico en application.properties llamado app.reports.default-provider. Con esto, el backend soporta e integra de forma nativa ambos esquemas simultáneamente a través de inyección condicional de propiedades:
# Cambiar de proveedor con un simple switch en producción
app.reports.default-provider=oracle

# Configuración de Oracle Cloud Object Storage
oracle.s3.accessKeyId=${ORACLE_ACCESS_KEY_ID}
oracle.s3.secretAccessKey=${ORACLE_SECRET_ACCESS_KEY}
oracle.s3.endpoint=https://tu-namespace.compat.objectstorage.us-ashburn-1.oraclecloud.com
oracle.s3.bucketName=tvapymm-reports-bucket

Gracias a este diseño de desacoplamiento de infraestructura, logramos migrar con éxito todo el ecosistema de TVAPyMM a Oracle Cloud en un fin de semana. El portal de producción ahora funciona de manera robusta a coste cero permanente y con un rendimiento de red y acceso a disco superior al ofrecido originalmente por la capa básica de AWS.