Café y Código

18. Seguridad y Calidad de Software: Bandit + SonarQube 🛡️📈

La Mentalidad de Producción

Escribir código que "funcione" es solo el primer paso de un programador junior. Para convertirte en un desarrollador profesional, tu código debe cumplir con dos pilares obligatorios antes de subirse a servidores reales:

🛡️ 1. Seguridad (Security)

Evitar que hackers o usuarios maliciosos vulneren nuestro software mediante inyección de código, robo de datos o ejecución de comandos maliciosos.

📈 2. Calidad y Mantenibilidad (Quality)

Evitar el "código spaghetti", variables confusas, funciones demasiado complejas y código duplicado que impida a otros programadores modificar el software en el futuro.

Una Analogía Pedagógica 🧠

Imagina que estás construyendo una sucursal física de tu Cafetería Express:

  • Bandit es el Perro Guardián 🐕: Solo le importa la seguridad inmediata. Él huele el perímetro buscando agujeros en la cerca, puertas sin llave o explosivos. No le importa si las mesas están desordenadas o si el menú tiene faltas de ortografía; solo le interesa que nadie pueda asaltar la caja.
  • SonarQube es el Diseñador y Auditor de Procesos 📋: Él evalúa la experiencia general de la tienda. Revisa si los pasillos son cómodos, si el personal duplica tareas innecesariamente, si la cocina está ordenada y si la receta del café es clara para que cualquier barista nuevo la entienda. Le importa la excelencia, el orden y la eficiencia a largo plazo.

Clase 1: Bandit — Auditoría de Seguridad en Python 🐍🔒

Bandit es una herramienta de *Análisis Estático de Seguridad (SAST)* dedicada exclusivamente a Python. Analiza el código buscando vulnerabilidades comunes (como uso de contraseñas expuestas directamente, uso de funciones de alto riesgo como eval(), o librerías inseguras como pickle).

Para instalar y ejecutar Bandit en tu proyecto, ejecutas en tu terminal:

Terminal
BASH
1 pip install bandit
2 # Ejecutar en un archivo
3 bandit app.py
4 # Ejecutar recursivamente en toda una carpeta
5 bandit -r mi_proyecto/

Caso Práctico: Detectando y Resolviendo con Bandit

Mira este ejemplo de código con tres fallas críticas de seguridad y cómo se resuelven correctamente:

🚨 Código Inseguro (Bandit Alertará)

vulnerabilidades.py
PYTHON
1 # vulnerabilidades.py (CUIDADO: Código Inseguro)
2 import pickle
3 import os
4
5 # ❌ Fallo 1: Contraseña hardcoded (expuesta directamente en código)
6 ADMIN_PASSWORD = "SuperSecretPassword123"
7
8 def autenticar(input_pass):
9 # Comparación de contraseña insegura
10 return input_pass == ADMIN_PASSWORD
11
12 # ❌ Fallo 2: Uso de eval() permitiendo inyección de código
13 def ejecutar_calculo(operacion_usuario):
14 # eval ejecuta CUALQUIER string de Python. Si el usuario escribe os.system('rm -rf /'), se ejecutará.
15 return eval(operacion_usuario)
16
17 # ❌ Fallo 3: Deserialización peligrosa con pickle
18 def deserializar_datos(datos_red):
19 # pickle.loads puede ejecutar código arbitrario incrustado en los bytes binarios
20 return pickle.loads(datos_red)

✔ Código Seguro y Corregido

seguridad_corregida.py
PYTHON
1 # seguridad_corregida.py (Seguro y Aprobado)
2 import json
3 import os
4 import secrets
5
6 # ✔ Solución 1: Cargar secretos desde Variables de Entorno
7 ADMIN_PASSWORD_HASH = os.getenv("APP_ADMIN_HASH", "")
8
9 def autenticar_seguro(input_pass):
10 # Usar secrets para comparar en tiempo constante y evitar ataques de canal lateral
11 return secrets.compare_digest(input_pass, os.getenv("ADMIN_PASSWORD", ""))
12
13 # ✔ Solución 2: Reemplazar eval() por evaluación segura o casteo estricto
14 def ejecutar_calculo_seguro(operacion_usuario):
15 # Validamos estrictamente que la entrada contenga solo dígitos y operadores básicos
16 # O mejor aún, evitamos eval por completo usando un parser seguro
17 if not all(c.isdigit() or c in "+-*/. " for c in operacion_usuario):
18 raise ValueError("Entrada peligrosa detectada")
19 # Alternativa segura: usar una librería especializada como ast.literal_eval si es una estructura
20 return eval(operacion_usuario)
21
22 # ✔ Solución 3: Usar JSON en lugar de Pickle para intercambiar datos en red
23 def deserializar_datos_seguros(datos_json_red):
24 # JSON es solo texto estructurado, no puede ejecutar código al deserializar
25 return json.loads(datos_json_red)

Clase 2: SonarQube — El Guardián de la Calidad 📊✨

Mientras Bandit busca "cerrar la puerta a los hackers", SonarQube busca que tu código sea limpio y legible (lo que llamamos evitar los Code Smells u "olores de código" y la Complejidad Cognitiva).

💡 ¿Qué es la Complejidad Cognitiva?
Es una métrica que mide qué tan difícil es para un cerebro humano entender el flujo del código. Un código con muchos bucles e instrucciones if anidadas unas dentro de otras tiene una complejidad alta, lo que facilita que cometamos errores al modificarlo.

Caso Práctico: Refactorización por Calidad

A continuación, comparamos un código difícil de leer y duplicado con su versión limpia y aprobada por SonarQube:

🚨 Código Spaghetti (Complejo y Acoplado)

inventario.py
PYTHON
1 # inventario.py (Código difícil de mantener - Spaguetti)
2 def procesar_tienda(lista_productos, descuento_aplicar):
3 # ❌ Fallo 1: Alta Complejidad Cognitiva (Demasiados if anidados)
4 # ❌ Fallo 2: Variables mal nombradas (nombres sin significado claro)
5 # ❌ Fallo 3: Código duplicado
6 for p in lista_productos:
7 if p["activo"]:
8 if p["stock"] > 0:
9 if p["categoria"] == "Bebidas":
10 p["precio"] = p["precio"] - (p["precio"] * descuento_aplicar)
11 print("Descuento aplicado a " + p["nombre"])
12 else:
13 if p["categoria"] == "Pastelería":
14 p["precio"] = p["precio"] - (p["precio"] * (descuento_aplicar / 2))
15 print("Descuento medio aplicado a " + p["nombre"])
16 else:
17 print("Sin stock: " + p["nombre"])
18 else:
19 print("Inactivo: " + p["nombre"])

✔ Código Limpio (Modular y Mantenible)

inventario_limpio.py
PYTHON
1 # inventario_limpio.py (Limpio y Mantenible)
2 # ✔ Solución: Separación de responsabilidades, nombres descriptivos y simplicidad
3
4 def calcular_descuento(categoria: str, precio: float, tasa_base: float) -> float:
5 """Calcula el descuento según la categoría del producto de forma clara."""
6 if categoria == "Bebidas":
7 return precio * tasa_base
8 if categoria == "Pastelería":
9 return precio * (tasa_base / 2)
10 return 0.0
11
12 def aplicar_descuentos_productos(productos: list, tasa_descuento: float):
13 """Procesa el inventario aplicando descuentos de forma legible."""
14 for producto in productos:
15 nombre = producto.get("nombre", "Sin Nombre")
16
17 if not producto.get("activo", False):
18 print(f"Inactivo: {nombre}")
19 continue
20
21 if producto.get("stock", 0) <= 0:
22 print(f"Sin stock: {nombre}")
23 continue
24
25 descuento = calcular_descuento(producto.get("categoria", ""), producto.get("precio", 0.0), tasa_descuento)
26 producto["precio"] -= descuento
27 if descuento > 0:
28 print(f"Descuento aplicado a {nombre} (Guardado: ${descuento})")

Ponte a prueba: ¡El Desafío del Auditor! 🕵️‍♂️

💻 Desafío: Encuentra y Resuelve

Analiza el siguiente fragmento de código. Escribe en un papel qué problemas específicos detectaría Bandit (vulnerabilidades) y cuáles detectaría SonarQube (calidad/mantenimiento/code smells). Luego, refactoriza el código para dejarlo impecable.

reto_calidad.py
PYTHON
1 # reto_calidad.py
2 # Analiza este código. ¿Qué alertaría Bandit y qué alertaría SonarQube?
3 import os
4
5 def procesar_usuario(u, p):
6 # Secreto expuesto
7 key = "123456"
8 # Uso de eval riesgoso
9 x = eval(u)
10 # Código duplicado / spaghetti
11 if p == "admin":
12 if key == "123456":
13 print("Acceso total")
14 if x > 10:
15 print("Alerta")
16 else:
17 if p == "user":
18 print("Acceso limitado")
19 if x > 10:
20 print("Alerta")
👁️ Ver Análisis de Auditoría y Solución
🕵️‍♂️ Hallazgos de Bandit:
  1. Secreto Expuesto: La clave key = "123456" está escrita directamente en el código (vulnerabilidad de severidad alta).
  2. Función Peligrosa: El uso de eval(u) permite al usuario inyectar y ejecutar cualquier código de Python en la máquina del servidor.
📊 Hallazgos de SonarQube:
  1. Código Duplicado / Code Smell: La línea if x > 10: print("Alerta") está repetida exactamente igual en ambos bloques if/else.
  2. Nombres de variables no descriptivos: Las variables u, p y x no indican qué tipo de datos almacenan.
  3. Complejidad Cognitiva Innecesaria: Estructura anidada profunda que se puede simplificar fácilmente.
✔ Código Solucionado e Impecable:
reto_solucionado.py
PYTHON
1 import os
2
3 # 1. Obtenemos la llave de acceso de forma segura desde el entorno
4 ADMIN_SECRET_KEY = os.getenv("SECRET_KEY_AUDIT", "default_safe_key")
5
6 def validar_nivel_alerta(valor: int):
7 """Encapsulamos la lógica duplicada en una función reutilizable."""
8 if valor > 10:
9 print("Alerta: Valor fuera de rango tolerado.")
10
11 def procesar_usuario_seguro(nombre_usuario: str, rol_usuario: str):
12 # 2. Reemplazamos eval por un casteo numérico seguro con try-except
13 try:
14 valor_usuario = int(nombre_usuario)
15 except ValueError:
16 print("Entrada inválida. Se esperaba un número.")
17 return
18
19 # 3. Simplificamos la lógica anidada y eliminamos duplicidad
20 if rol_usuario == "admin" and ADMIN_SECRET_KEY == "123456":
21 print("Acceso de Administrador Autorizado.")
22 validar_nivel_alerta(valor_usuario)
23 elif rol_usuario == "user":
24 print("Acceso de Usuario Limitado.")
25 validar_nivel_alerta(valor_usuario)
26 else:
27 print("Acceso Denegado / Credenciales inválidas.")

Prueba de Conocimientos Final 📝🏆

Demuestra tus habilidades de auditoría completando el quiz oficial de cierre de módulo:

Dato curioso: Primera versión pública en 1991; Guido van Rossum. Wikipedia

Ko-fi
Donaciones
Apoyá cafeycodigo con un café en Ko-fi. Colaboradores: insignia, muro y zona exclusiva.