feat: Version 1.11.0 - Mejoras en auditoría, SLA, frontend y correcciones de sincronización
- Refactorización de endpoints de auditoría y helpers - Mejoras en esquemas de auditoría (audit.py) - Correcciones en endpoint SLA - Actualizaciones en múltiples rutas del frontend interno: layout, tickets, usuarios, tenants, categorías, sistemas, SLA (at-risk, violations), auditoría (main + security), login, perfil - Actualización de tailwind.config.js - Eliminación de docs de versiones anteriores (CAMBIOS_v1.10.0, v1.8.0, OPTIMIZACIONES) - Nuevos scripts de prueba: generate_security_test_data.py, generate_sla_test_data.py - Script de prueba de sincronización crítica (test_critical_sync.ps1) - README actualizado en scripts/
This commit is contained in:
193
backend/scripts/README_SECURITY_TESTS.md
Normal file
193
backend/scripts/README_SECURITY_TESTS.md
Normal file
@@ -0,0 +1,193 @@
|
||||
# Scripts de Prueba de Seguridad
|
||||
|
||||
Scripts para generar datos de prueba para el análisis de seguridad.
|
||||
|
||||
## 📋 Scripts Disponibles
|
||||
|
||||
### 1. `generate_security_test_data.py`
|
||||
|
||||
Genera un conjunto completo de logs de auditoría para probar todas las funcionalidades del análisis de seguridad.
|
||||
|
||||
#### Uso
|
||||
|
||||
```bash
|
||||
# Asegúrate de estar en el entorno virtual
|
||||
cd backend
|
||||
python scripts/generate_security_test_data.py
|
||||
```
|
||||
|
||||
#### Qué Genera
|
||||
|
||||
- **25 intentos fallidos de login** → Amenaza HIGH de fuerza bruta
|
||||
- **55 eliminaciones masivas** → Amenaza CRITICAL
|
||||
- **5 cambios de privilegios** → Amenaza HIGH de escalación de privilegios
|
||||
- **20 logs normales** → Actividad regular para contexto
|
||||
|
||||
#### Limpiar Datos de Prueba
|
||||
|
||||
```bash
|
||||
python scripts/generate_security_test_data.py cleanup
|
||||
```
|
||||
|
||||
Esto eliminará **TODOS** los logs de auditoría de las últimas 24 horas.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Escenarios de Prueba
|
||||
|
||||
### Escenario 1: Sistema Limpio (Sin Amenazas)
|
||||
|
||||
```bash
|
||||
# Limpiar todos los logs
|
||||
python scripts/generate_security_test_data.py cleanup
|
||||
```
|
||||
|
||||
**Resultado esperado:**
|
||||
- Dashboard con todos los contadores en 0
|
||||
- Tab "Crítico" vacío
|
||||
- Mensaje: "Sistema Seguro"
|
||||
|
||||
---
|
||||
|
||||
### Escenario 2: Solo Amenazas Leves
|
||||
|
||||
Modifica el script para generar solo 6 intentos fallidos (MEDIUM severity):
|
||||
|
||||
```python
|
||||
# En generate_security_test_data.py, línea ~70
|
||||
for i in range(6): # Cambiar de 25 a 6
|
||||
```
|
||||
|
||||
**Resultado esperado:**
|
||||
- 1 amenaza MEDIUM en tab correspondiente
|
||||
- Tab "Crítico" vacío
|
||||
- Nivel de riesgo: LOW o MEDIUM
|
||||
|
||||
---
|
||||
|
||||
### Escenario 3: Amenazas Críticas
|
||||
|
||||
Ejecuta el script completo:
|
||||
|
||||
```bash
|
||||
python scripts/generate_security_test_data.py
|
||||
```
|
||||
|
||||
**Resultado esperado:**
|
||||
- 1 amenaza CRÍTICA (eliminaciones masivas)
|
||||
- 2 amenazas HIGH (login fallidos + privilegios)
|
||||
- Tab "Crítico" con 1 amenaza
|
||||
- Nivel de riesgo: CRITICAL
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Umbrales de Detección
|
||||
|
||||
| Tipo de Amenaza | Umbral Detección | Severidades |
|
||||
|-----------------|------------------|-------------|
|
||||
| **Fuerza Bruta** | ≥5 intentos fallidos | MEDIUM (5-19), HIGH (≥20) |
|
||||
| **Eliminaciones Masivas** | ≥10 eliminaciones | HIGH (10-49), **CRITICAL (≥50)** |
|
||||
| **Cambios de Privilegios** | ≥3 cambios de rol | HIGH (siempre) |
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Verificar Resultados
|
||||
|
||||
1. **Accede al panel de auditoría**: http://localhost:3001/audit/security
|
||||
|
||||
2. **Verifica los contadores del dashboard:**
|
||||
- Nivel de Riesgo
|
||||
- Amenazas Detectadas
|
||||
- Intentos Fallidos
|
||||
- IPs Sospechosas
|
||||
- Acciones Críticas
|
||||
|
||||
3. **Prueba los tabs:**
|
||||
- Todas: Debe mostrar amenazas activas
|
||||
- Crítico: Solo amenazas critical (si hay)
|
||||
- High: Amenazas de alta severidad
|
||||
- Medium: Amenazas de severidad media
|
||||
- Low: Amenazas de baja severidad
|
||||
- Resueltas: Amenazas marcadas como resueltas
|
||||
|
||||
4. **Prueba la búsqueda:**
|
||||
- Busca por IP: `192.168.1.100`
|
||||
- Busca por descripción: `intentos fallidos`
|
||||
- Busca por tipo: `brute_force`
|
||||
|
||||
5. **Prueba los filtros:**
|
||||
- Filtra por tipo de amenaza
|
||||
- Combina búsqueda + filtro
|
||||
|
||||
6. **Prueba las acciones:**
|
||||
- Selecciona múltiples amenazas
|
||||
- Resuelve en batch
|
||||
- Marca como resuelta individualmente
|
||||
- Reabre amenazas resueltas
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Advertencias
|
||||
|
||||
- **NO ejecutar en producción**: Estos scripts son SOLO para desarrollo/testing
|
||||
- **Los datos son ficticios**: IPs, usuarios y acciones son simulados
|
||||
- **Cleanup elimina TODO**: El comando cleanup elimina TODOS los logs de las últimas 24h, no solo los de prueba
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Error: "No se encontró ningún tenant"
|
||||
```bash
|
||||
# Ejecuta las migraciones
|
||||
cd backend
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
### Error: "No se encontró ningún usuario"
|
||||
```bash
|
||||
# Crea un usuario de prueba
|
||||
python scripts/create_test_user.py
|
||||
```
|
||||
|
||||
### La página no muestra amenazas
|
||||
- Verifica que el backend esté corriendo: `uvicorn app.main:app --reload`
|
||||
- Revisa la consola del navegador para errores
|
||||
- Verifica que los logs se crearon: `SELECT COUNT(*) FROM audit_logs WHERE created_at >= NOW() - INTERVAL '24 hours';`
|
||||
|
||||
### Las fechas no son de hoy
|
||||
- Los logs se crean con timestamps aleatorios en las últimas 24h
|
||||
- Si todos tienen la misma fecha, es porque se generaron en el mismo segundo (normal)
|
||||
|
||||
---
|
||||
|
||||
## 📝 Personalizar Generación
|
||||
|
||||
Para crear escenarios personalizados, edita `generate_security_test_data.py`:
|
||||
|
||||
```python
|
||||
# Cambiar cantidad de intentos fallidos
|
||||
for i in range(50): # Más intentos = mayor severidad
|
||||
|
||||
# Cambiar IPs sospechosas
|
||||
suspicious_ips = ["1.2.3.4", "5.6.7.8"]
|
||||
|
||||
# Cambiar período temporal
|
||||
time_offset = timedelta(hours=12) # Todos en las últimas 12h
|
||||
|
||||
# Agregar más tipos de amenazas
|
||||
# Agrega nuevos bloques de generación siguiendo el patrón
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Flujo Recomendado de Prueba
|
||||
|
||||
1. **Limpia el sistema**: `python scripts/generate_security_test_data.py cleanup`
|
||||
2. **Verifica sistema limpio**: Accede a la página, debe estar vacía
|
||||
3. **Genera datos completos**: `python scripts/generate_security_test_data.py`
|
||||
4. **Prueba todas las funcionalidades**: tabs, filtros, búsqueda, acciones
|
||||
5. **Marca algunas como resueltas**: Prueba el flujo de resolución
|
||||
6. **Verifica tab "Resueltas"**: Confirma que aparecen ahí
|
||||
7. **Reabre algunas**: Prueba el flujo de reapertura
|
||||
8. **Limpia al finalizar**: `python scripts/generate_security_test_data.py cleanup`
|
||||
240
backend/scripts/generate_security_test_data.py
Normal file
240
backend/scripts/generate_security_test_data.py
Normal file
@@ -0,0 +1,240 @@
|
||||
"""
|
||||
Script para generar datos de prueba de seguridad en logs de auditoría.
|
||||
Esto permite probar la funcionalidad de análisis de seguridad con diferentes tipos de amenazas.
|
||||
"""
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import uuid
|
||||
import random
|
||||
|
||||
# Agregar el directorio raíz al path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from app.core.database import AsyncSessionLocal
|
||||
from app.models.audit import AuditLog
|
||||
from app.models.user import User
|
||||
from app.models.tenant import Tenant
|
||||
|
||||
|
||||
async def generate_test_data():
|
||||
"""Genera logs de auditoría de prueba para análisis de seguridad."""
|
||||
async with AsyncSessionLocal() as db:
|
||||
# Obtener tenant y usuarios de prueba
|
||||
tenant_result = await db.execute(select(Tenant).limit(1))
|
||||
tenant = tenant_result.scalar_one_or_none()
|
||||
|
||||
if not tenant:
|
||||
print("❌ No se encontró ningún tenant. Ejecuta las migraciones primero.")
|
||||
return
|
||||
|
||||
user_result = await db.execute(select(User).where(User.tenant_id == tenant.id).limit(1))
|
||||
user = user_result.scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
print("❌ No se encontró ningún usuario. Crea un usuario primero.")
|
||||
return
|
||||
|
||||
print(f"✅ Usando tenant: {tenant.name}")
|
||||
print(f"✅ Usando usuario: {user.email}")
|
||||
print()
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
# IPs de prueba
|
||||
suspicious_ips = [
|
||||
"192.168.1.100",
|
||||
"10.0.0.50",
|
||||
"172.16.0.10",
|
||||
"203.0.113.42",
|
||||
"198.51.100.88"
|
||||
]
|
||||
|
||||
logs_created = 0
|
||||
|
||||
# ============================================
|
||||
# 1. GENERAR INTENTOS FALLIDOS DE LOGIN (Fuerza Bruta)
|
||||
# ============================================
|
||||
print("🔐 Generando intentos fallidos de login...")
|
||||
|
||||
# Generar 25 intentos fallidos (esto hará que sea HIGH severity)
|
||||
for i in range(25):
|
||||
time_offset = timedelta(hours=random.randint(0, 23), minutes=random.randint(0, 59))
|
||||
log = AuditLog(
|
||||
id=uuid.uuid4(),
|
||||
tenant_id=tenant.id,
|
||||
user_id=user.id,
|
||||
action="user.login_failed",
|
||||
resource_type="auth",
|
||||
resource_id=None,
|
||||
ip_address=random.choice(suspicious_ips),
|
||||
user_agent="Mozilla/5.0 (Test Browser)",
|
||||
metadata={"reason": "invalid_credentials", "username": f"test_user_{i}"},
|
||||
created_at=now - time_offset
|
||||
)
|
||||
db.add(log)
|
||||
logs_created += 1
|
||||
|
||||
print(f" ✓ Creados {25} intentos fallidos de login (HIGH severity)")
|
||||
|
||||
# ============================================
|
||||
# 2. GENERAR ELIMINACIONES MASIVAS (CRITICAL)
|
||||
# ============================================
|
||||
print("🗑️ Generando eliminaciones masivas...")
|
||||
|
||||
resources = ["ticket", "comment", "attachment", "category", "user"]
|
||||
|
||||
# Generar 55 eliminaciones (esto hará que sea CRITICAL severity)
|
||||
for i in range(55):
|
||||
time_offset = timedelta(hours=random.randint(0, 23), minutes=random.randint(0, 59))
|
||||
resource = random.choice(resources)
|
||||
log = AuditLog(
|
||||
id=uuid.uuid4(),
|
||||
tenant_id=tenant.id,
|
||||
user_id=user.id,
|
||||
action=f"{resource}.delete",
|
||||
resource_type=resource,
|
||||
resource_id=uuid.uuid4(),
|
||||
ip_address=random.choice(suspicious_ips),
|
||||
user_agent="Mozilla/5.0 (Test Browser)",
|
||||
metadata={"deleted_by": user.email},
|
||||
created_at=now - time_offset
|
||||
)
|
||||
db.add(log)
|
||||
logs_created += 1
|
||||
|
||||
print(f" ✓ Creadas {55} eliminaciones masivas (CRITICAL severity)")
|
||||
|
||||
# ============================================
|
||||
# 3. GENERAR CAMBIOS DE PRIVILEGIOS (HIGH)
|
||||
# ============================================
|
||||
print("👤 Generando cambios de privilegios...")
|
||||
|
||||
roles = ["AGENT", "CLIENT_USER", "AUDITOR", "SUPPORT_MANAGER", "ADMIN"]
|
||||
|
||||
# Generar 5 cambios de rol (esto hará que sea HIGH severity)
|
||||
for i in range(5):
|
||||
time_offset = timedelta(hours=random.randint(0, 23), minutes=random.randint(0, 59))
|
||||
old_role = random.choice(roles)
|
||||
new_role = random.choice([r for r in roles if r != old_role])
|
||||
|
||||
log = AuditLog(
|
||||
id=uuid.uuid4(),
|
||||
tenant_id=tenant.id,
|
||||
user_id=user.id,
|
||||
action="user.update",
|
||||
resource_type="user",
|
||||
resource_id=uuid.uuid4(),
|
||||
ip_address=random.choice(suspicious_ips),
|
||||
user_agent="Mozilla/5.0 (Test Browser)",
|
||||
old_values={"role": old_role},
|
||||
new_values={"role": new_role},
|
||||
metadata={"changed_by": user.email},
|
||||
created_at=now - time_offset
|
||||
)
|
||||
db.add(log)
|
||||
logs_created += 1
|
||||
|
||||
print(f" ✓ Creados {5} cambios de privilegios (HIGH severity)")
|
||||
|
||||
# ============================================
|
||||
# 4. GENERAR LOGS NORMALES (para dar contexto)
|
||||
# ============================================
|
||||
print("📋 Generando logs de actividad normal...")
|
||||
|
||||
normal_actions = [
|
||||
"ticket.create",
|
||||
"ticket.update",
|
||||
"comment.create",
|
||||
"user.login",
|
||||
"ticket.view",
|
||||
]
|
||||
|
||||
for i in range(20):
|
||||
time_offset = timedelta(hours=random.randint(0, 23), minutes=random.randint(0, 59))
|
||||
action = random.choice(normal_actions)
|
||||
|
||||
log = AuditLog(
|
||||
id=uuid.uuid4(),
|
||||
tenant_id=tenant.id,
|
||||
user_id=user.id,
|
||||
action=action,
|
||||
resource_type=action.split('.')[0],
|
||||
resource_id=uuid.uuid4(),
|
||||
ip_address=random.choice(suspicious_ips),
|
||||
user_agent="Mozilla/5.0 (Test Browser)",
|
||||
metadata={"action": "normal_activity"},
|
||||
created_at=now - time_offset
|
||||
)
|
||||
db.add(log)
|
||||
logs_created += 1
|
||||
|
||||
print(f" ✓ Creados {20} logs de actividad normal")
|
||||
|
||||
# Guardar todo
|
||||
await db.commit()
|
||||
|
||||
print()
|
||||
print("=" * 60)
|
||||
print(f"✅ GENERACIÓN COMPLETADA")
|
||||
print(f" Total de logs creados: {logs_created}")
|
||||
print()
|
||||
print("📊 Amenazas esperadas en el análisis:")
|
||||
print(" 🔴 1 amenaza CRÍTICA: 55 eliminaciones masivas")
|
||||
print(" 🟠 1 amenaza HIGH: 25 intentos fallidos de login")
|
||||
print(" 🟠 1 amenaza HIGH: 5 cambios de privilegios")
|
||||
print()
|
||||
print("🌐 Accede a la página de seguridad para ver el análisis")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
async def cleanup_test_data():
|
||||
"""Elimina los logs de auditoría de prueba."""
|
||||
async with AsyncSessionLocal() as db:
|
||||
tenant_result = await db.execute(select(Tenant).limit(1))
|
||||
tenant = tenant_result.scalar_one_or_none()
|
||||
|
||||
if not tenant:
|
||||
print("❌ No se encontró ningún tenant.")
|
||||
return
|
||||
|
||||
# Eliminar logs de las últimas 24 horas
|
||||
now = datetime.now(timezone.utc)
|
||||
cutoff = now - timedelta(hours=24)
|
||||
|
||||
result = await db.execute(
|
||||
select(AuditLog).where(
|
||||
AuditLog.tenant_id == tenant.id,
|
||||
AuditLog.created_at >= cutoff
|
||||
)
|
||||
)
|
||||
logs = result.scalars().all()
|
||||
|
||||
if not logs:
|
||||
print("ℹ️ No hay logs de prueba para eliminar.")
|
||||
return
|
||||
|
||||
for log in logs:
|
||||
await db.delete(log)
|
||||
|
||||
await db.commit()
|
||||
|
||||
print(f"✅ Eliminados {len(logs)} logs de prueba de las últimas 24 horas")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "cleanup":
|
||||
print("🧹 Limpiando datos de prueba...")
|
||||
asyncio.run(cleanup_test_data())
|
||||
else:
|
||||
print("🚀 Generando datos de prueba para análisis de seguridad...")
|
||||
print()
|
||||
asyncio.run(generate_test_data())
|
||||
print()
|
||||
print("💡 Para limpiar estos datos de prueba, ejecuta:")
|
||||
print(" python scripts/generate_security_test_data.py cleanup")
|
||||
399
backend/scripts/generate_sla_test_data.py
Normal file
399
backend/scripts/generate_sla_test_data.py
Normal file
@@ -0,0 +1,399 @@
|
||||
"""
|
||||
Script para generar datos de prueba de SLA Management.
|
||||
Crea tickets con diferentes estados de SLA para probar el dashboard.
|
||||
"""
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import uuid
|
||||
import random
|
||||
|
||||
# Agregar el directorio raíz al path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from app.core.database import AsyncSessionLocal
|
||||
from app.models.ticket import Ticket, TicketStatus, TicketPriority
|
||||
from app.models.category import Category
|
||||
from app.models.user import User
|
||||
from app.models.tenant import Tenant
|
||||
from app.models.system import System
|
||||
|
||||
|
||||
async def generate_sla_test_data():
|
||||
"""Genera tickets de prueba con diferentes estados de SLA."""
|
||||
async with AsyncSessionLocal() as db:
|
||||
# Obtener tenant y usuarios
|
||||
tenant_result = await db.execute(select(Tenant).limit(1))
|
||||
tenant = tenant_result.scalar_one_or_none()
|
||||
|
||||
if not tenant:
|
||||
print("❌ No se encontró ningún tenant. Ejecuta las migraciones primero.")
|
||||
return
|
||||
|
||||
# Obtener usuarios
|
||||
users_result = await db.execute(
|
||||
select(User).where(User.tenant_id == tenant.id).limit(5)
|
||||
)
|
||||
users = list(users_result.scalars().all())
|
||||
|
||||
if not users:
|
||||
print("❌ No se encontraron usuarios. Crea usuarios primero.")
|
||||
return
|
||||
|
||||
creator = users[0]
|
||||
agents = users if len(users) > 1 else [creator]
|
||||
|
||||
# Obtener o crear categorías
|
||||
categories_result = await db.execute(
|
||||
select(Category).where(Category.tenant_id == tenant.id)
|
||||
)
|
||||
categories = list(categories_result.scalars().all())
|
||||
|
||||
if not categories:
|
||||
print("📁 Creando categorías de prueba...")
|
||||
category_data = [
|
||||
{"name": "Soporte Técnico", "sla_response_hours": 2, "sla_resolution_hours": 24, "color": "#3B82F6"},
|
||||
{"name": "Facturación", "sla_response_hours": 4, "sla_resolution_hours": 48, "color": "#10B981"},
|
||||
{"name": "Incidente Crítico", "sla_response_hours": 1, "sla_resolution_hours": 8, "color": "#EF4444"},
|
||||
{"name": "Consulta General", "sla_response_hours": 8, "sla_resolution_hours": 72, "color": "#6B7280"},
|
||||
]
|
||||
|
||||
for cat_data in category_data:
|
||||
category = Category(
|
||||
id=uuid.uuid4(),
|
||||
tenant_id=tenant.id,
|
||||
name=cat_data["name"],
|
||||
description=f"Categoría de {cat_data['name']}",
|
||||
color=cat_data["color"],
|
||||
sla_response_hours=cat_data["sla_response_hours"],
|
||||
sla_resolution_hours=cat_data["sla_resolution_hours"],
|
||||
is_active=True
|
||||
)
|
||||
db.add(category)
|
||||
categories.append(category)
|
||||
|
||||
await db.commit()
|
||||
print(f" ✓ Creadas {len(categories)} categorías")
|
||||
|
||||
# Obtener o crear sistemas afectados
|
||||
systems_result = await db.execute(
|
||||
select(System).where(System.tenant_id == tenant.id)
|
||||
)
|
||||
systems = list(systems_result.scalars().all())
|
||||
|
||||
if not systems:
|
||||
print("🖥️ Creando sistemas de prueba...")
|
||||
system_names = ["Portal Web", "API REST", "Base de Datos", "Sistema de Pagos"]
|
||||
for sys_name in system_names:
|
||||
system = System(
|
||||
id=uuid.uuid4(),
|
||||
tenant_id=tenant.id,
|
||||
name=sys_name,
|
||||
description=f"Sistema {sys_name}",
|
||||
is_active=True
|
||||
)
|
||||
db.add(system)
|
||||
systems.append(system)
|
||||
|
||||
await db.commit()
|
||||
print(f" ✓ Creados {len(systems)} sistemas")
|
||||
|
||||
print(f"✅ Usando tenant: {tenant.name}")
|
||||
print(f"✅ Usuarios disponibles: {len(users)}")
|
||||
print(f"✅ Categorías disponibles: {len(categories)}")
|
||||
print()
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
tickets_created = 0
|
||||
|
||||
# Función auxiliar para crear ticket
|
||||
def create_ticket(
|
||||
subject: str,
|
||||
description: str,
|
||||
priority: TicketPriority,
|
||||
status: TicketStatus,
|
||||
category: Category,
|
||||
created_hours_ago: int,
|
||||
first_response_hours_after: int = None,
|
||||
resolved_hours_after: int = None,
|
||||
assigned: bool = True
|
||||
):
|
||||
nonlocal tickets_created
|
||||
|
||||
ticket_id = uuid.uuid4()
|
||||
created_at = now - timedelta(hours=created_hours_ago)
|
||||
|
||||
# Calcular SLA deadlines basados en la categoría (sin timezone para la BD)
|
||||
sla_response_due = (created_at + timedelta(hours=category.sla_response_hours)).replace(tzinfo=None)
|
||||
sla_resolution_due = (created_at + timedelta(hours=category.sla_resolution_hours)).replace(tzinfo=None)
|
||||
|
||||
# Primera respuesta (si aplica)
|
||||
first_response_at = None
|
||||
if first_response_hours_after is not None:
|
||||
first_response_at = (created_at + timedelta(hours=first_response_hours_after)).replace(tzinfo=None)
|
||||
|
||||
# Resolución (si aplica)
|
||||
resolved_at = None
|
||||
if resolved_hours_after is not None:
|
||||
resolved_at = (created_at + timedelta(hours=resolved_hours_after)).replace(tzinfo=None)
|
||||
|
||||
ticket = Ticket(
|
||||
id=ticket_id,
|
||||
tenant_id=tenant.id,
|
||||
ticket_number=f"TKT-{1000 + tickets_created}",
|
||||
subject=subject,
|
||||
description=description,
|
||||
status=status,
|
||||
priority=priority,
|
||||
created_by=creator.id,
|
||||
assigned_to=random.choice(agents).id if assigned else None,
|
||||
category_id=category.id,
|
||||
affected_system_id=random.choice(systems).id if systems else None,
|
||||
sla_response_due=sla_response_due,
|
||||
sla_resolution_due=sla_resolution_due,
|
||||
first_response_at=first_response_at,
|
||||
resolved_at=resolved_at,
|
||||
created_at=created_at,
|
||||
updated_at=resolved_at or first_response_at or created_at
|
||||
)
|
||||
|
||||
db.add(ticket)
|
||||
tickets_created += 1
|
||||
return ticket
|
||||
|
||||
# ============================================
|
||||
# 1. TICKETS CUMPLIENDO SLA RESPONSE (Verde)
|
||||
# ============================================
|
||||
print("✅ Generando tickets CUMPLIENDO Response SLA...")
|
||||
|
||||
for i in range(15):
|
||||
category = random.choice(categories)
|
||||
priority = random.choice([TicketPriority.LOW, TicketPriority.MEDIUM, TicketPriority.HIGH])
|
||||
|
||||
# Creado hace X horas, respondido ANTES del deadline
|
||||
created_hours_ago = random.randint(24, 120)
|
||||
response_time = random.uniform(0.5, category.sla_response_hours * 0.7) # 70% del SLA
|
||||
|
||||
status = random.choice([TicketStatus.IN_PROGRESS, TicketStatus.WAITING_CUSTOMER])
|
||||
|
||||
create_ticket(
|
||||
subject=f"Ticket con respuesta a tiempo #{i+1}",
|
||||
description=f"Este ticket fue respondido dentro del SLA de {category.name}",
|
||||
priority=priority,
|
||||
status=status,
|
||||
category=category,
|
||||
created_hours_ago=created_hours_ago,
|
||||
first_response_hours_after=response_time,
|
||||
assigned=True
|
||||
)
|
||||
|
||||
print(f" ✓ Creados 15 tickets cumpliendo Response SLA")
|
||||
|
||||
# ============================================
|
||||
# 2. TICKETS VIOLANDO SLA RESPONSE (Rojo)
|
||||
# ============================================
|
||||
print("🔴 Generando tickets VIOLANDO Response SLA...")
|
||||
|
||||
for i in range(8):
|
||||
category = random.choice(categories)
|
||||
priority = random.choice([TicketPriority.HIGH, TicketPriority.URGENT])
|
||||
|
||||
# Creado hace más tiempo que el SLA, SIN respuesta
|
||||
created_hours_ago = category.sla_response_hours + random.randint(1, 10)
|
||||
|
||||
create_ticket(
|
||||
subject=f"Ticket SIN respuesta - VIOLACIÓN #{i+1}",
|
||||
description=f"Este ticket lleva {created_hours_ago}h sin respuesta (SLA: {category.sla_response_hours}h)",
|
||||
priority=priority,
|
||||
status=random.choice([TicketStatus.NEW, TicketStatus.TRIAGE]),
|
||||
category=category,
|
||||
created_hours_ago=created_hours_ago,
|
||||
first_response_hours_after=None, # Sin respuesta!
|
||||
assigned=random.choice([True, False])
|
||||
)
|
||||
|
||||
print(f" ✓ Creados 8 tickets VIOLANDO Response SLA")
|
||||
|
||||
# ============================================
|
||||
# 3. TICKETS EN RIESGO Response (Amarillo)
|
||||
# ============================================
|
||||
print("⚠️ Generando tickets EN RIESGO Response SLA...")
|
||||
|
||||
for i in range(10):
|
||||
category = random.choice(categories)
|
||||
priority = random.choice([TicketPriority.MEDIUM, TicketPriority.HIGH, TicketPriority.URGENT])
|
||||
|
||||
# Creado hace tiempo, cerca del deadline (80-95% consumido)
|
||||
sla_hours = category.sla_response_hours
|
||||
time_consumed = random.uniform(0.8, 0.95) * sla_hours
|
||||
created_hours_ago = time_consumed
|
||||
|
||||
create_ticket(
|
||||
subject=f"Ticket cerca de vencer respuesta #{i+1}",
|
||||
description=f"Este ticket está al {int(time_consumed/sla_hours*100)}% del SLA de respuesta",
|
||||
priority=priority,
|
||||
status=random.choice([TicketStatus.TRIAGE, TicketStatus.NEW]),
|
||||
category=category,
|
||||
created_hours_ago=created_hours_ago,
|
||||
first_response_hours_after=None, # Aún sin respuesta
|
||||
assigned=True
|
||||
)
|
||||
|
||||
print(f" ✓ Creados 10 tickets EN RIESGO Response SLA")
|
||||
|
||||
# ============================================
|
||||
# 4. TICKETS CUMPLIENDO SLA RESOLUTION
|
||||
# ============================================
|
||||
print("✅ Generando tickets CUMPLIENDO Resolution SLA...")
|
||||
|
||||
for i in range(20):
|
||||
category = random.choice(categories)
|
||||
priority = random.choice([TicketPriority.LOW, TicketPriority.MEDIUM, TicketPriority.HIGH])
|
||||
|
||||
# Creado, respondido y resuelto dentro del SLA
|
||||
created_hours_ago = random.randint(72, 240)
|
||||
response_time = random.uniform(1, category.sla_response_hours * 0.5)
|
||||
resolution_time = random.uniform(
|
||||
response_time + 1,
|
||||
category.sla_resolution_hours * 0.8
|
||||
)
|
||||
|
||||
create_ticket(
|
||||
subject=f"Ticket resuelto a tiempo #{i+1}",
|
||||
description=f"Este ticket fue resuelto dentro del SLA de {category.name}",
|
||||
priority=priority,
|
||||
status=random.choice([TicketStatus.RESOLVED, TicketStatus.CLOSED]),
|
||||
category=category,
|
||||
created_hours_ago=created_hours_ago,
|
||||
first_response_hours_after=response_time,
|
||||
resolved_hours_after=resolution_time,
|
||||
assigned=True
|
||||
)
|
||||
|
||||
print(f" ✓ Creados 20 tickets cumpliendo Resolution SLA")
|
||||
|
||||
# ============================================
|
||||
# 5. TICKETS VIOLANDO SLA RESOLUTION
|
||||
# ============================================
|
||||
print("🔴 Generando tickets VIOLANDO Resolution SLA...")
|
||||
|
||||
for i in range(6):
|
||||
category = random.choice(categories)
|
||||
priority = random.choice([TicketPriority.HIGH, TicketPriority.URGENT])
|
||||
|
||||
# Creado hace más del SLA de resolución, con respuesta pero sin resolver
|
||||
created_hours_ago = category.sla_resolution_hours + random.randint(5, 48)
|
||||
response_time = random.uniform(1, category.sla_response_hours * 0.5)
|
||||
|
||||
create_ticket(
|
||||
subject=f"Ticket sin resolver - VIOLACIÓN #{i+1}",
|
||||
description=f"Ticket lleva {created_hours_ago}h sin resolver (SLA: {category.sla_resolution_hours}h)",
|
||||
priority=priority,
|
||||
status=random.choice([TicketStatus.IN_PROGRESS, TicketStatus.WAITING_CUSTOMER]),
|
||||
category=category,
|
||||
created_hours_ago=created_hours_ago,
|
||||
first_response_hours_after=response_time,
|
||||
resolved_hours_after=None, # Sin resolver!
|
||||
assigned=True
|
||||
)
|
||||
|
||||
print(f" ✓ Creados 6 tickets VIOLANDO Resolution SLA")
|
||||
|
||||
# ============================================
|
||||
# 6. TICKETS EN RIESGO Resolution
|
||||
# ============================================
|
||||
print("⚠️ Generando tickets EN RIESGO Resolution SLA...")
|
||||
|
||||
for i in range(12):
|
||||
category = random.choice(categories)
|
||||
priority = random.choice([TicketPriority.MEDIUM, TicketPriority.HIGH])
|
||||
|
||||
# Con respuesta, cerca del deadline de resolución
|
||||
sla_hours = category.sla_resolution_hours
|
||||
time_consumed = random.uniform(0.75, 0.95) * sla_hours
|
||||
created_hours_ago = time_consumed
|
||||
response_time = random.uniform(0.5, category.sla_response_hours * 0.5)
|
||||
|
||||
create_ticket(
|
||||
subject=f"Ticket cerca de vencer resolución #{i+1}",
|
||||
description=f"Este ticket está al {int(time_consumed/sla_hours*100)}% del SLA de resolución",
|
||||
priority=priority,
|
||||
status=TicketStatus.IN_PROGRESS,
|
||||
category=category,
|
||||
created_hours_ago=created_hours_ago,
|
||||
first_response_hours_after=response_time,
|
||||
resolved_hours_after=None,
|
||||
assigned=True
|
||||
)
|
||||
|
||||
print(f" ✓ Creados 12 tickets EN RIESGO Resolution SLA")
|
||||
|
||||
# Guardar todos los tickets
|
||||
await db.commit()
|
||||
|
||||
print()
|
||||
print("=" * 70)
|
||||
print("✅ GENERACIÓN DE DATOS SLA COMPLETADA")
|
||||
print(f" Total de tickets creados: {tickets_created}")
|
||||
print()
|
||||
print("📊 Distribución esperada:")
|
||||
print(" ✅ Response cumplidos: 15 tickets")
|
||||
print(" 🔴 Response violados: 8 tickets")
|
||||
print(" ⚠️ Response en riesgo: 10 tickets")
|
||||
print(" ✅ Resolution cumplidos: 20 tickets")
|
||||
print(" 🔴 Resolution violados: 6 tickets")
|
||||
print(" ⚠️ Resolution en riesgo: 12 tickets")
|
||||
print()
|
||||
print("🌐 Ve los resultados en:")
|
||||
print(" Dashboard SLA: http://localhost:3001/sla")
|
||||
print("=" * 70)
|
||||
|
||||
|
||||
async def cleanup_sla_test_data():
|
||||
"""Elimina tickets de prueba."""
|
||||
async with AsyncSessionLocal() as db:
|
||||
tenant_result = await db.execute(select(Tenant).limit(1))
|
||||
tenant = tenant_result.scalar_one_or_none()
|
||||
|
||||
if not tenant:
|
||||
print("❌ No se encontró ningún tenant.")
|
||||
return
|
||||
|
||||
# Eliminar tickets que empiezan con TKT-
|
||||
result = await db.execute(
|
||||
select(Ticket).where(
|
||||
Ticket.tenant_id == tenant.id,
|
||||
Ticket.ticket_number.like('TKT-%')
|
||||
)
|
||||
)
|
||||
tickets = result.scalars().all()
|
||||
|
||||
if not tickets:
|
||||
print("ℹ️ No hay tickets de prueba para eliminar.")
|
||||
return
|
||||
|
||||
for ticket in tickets:
|
||||
await db.delete(ticket)
|
||||
|
||||
await db.commit()
|
||||
|
||||
print(f"✅ Eliminados {len(tickets)} tickets de prueba")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "cleanup":
|
||||
print("🧹 Limpiando datos de prueba de SLA...")
|
||||
print()
|
||||
asyncio.run(cleanup_sla_test_data())
|
||||
else:
|
||||
print("🚀 Generando datos de prueba para SLA Management...")
|
||||
print()
|
||||
asyncio.run(generate_sla_test_data())
|
||||
print()
|
||||
print("💡 Para limpiar estos datos de prueba, ejecuta:")
|
||||
print(" python scripts/generate_sla_test_data.py cleanup")
|
||||
Reference in New Issue
Block a user