diff --git a/backend/app/api/schemas/audit.py b/backend/app/api/schemas/audit.py index 1d31d0a..fcd101d 100644 --- a/backend/app/api/schemas/audit.py +++ b/backend/app/api/schemas/audit.py @@ -51,6 +51,55 @@ class AuditLogResponse(AuditLogBase): from_attributes = True +# =================================== +# SECURITY ANALYSIS SCHEMAS +# =================================== + +class SecurityThreatPattern(BaseModel): + """Patrón de amenaza detectado.""" + type: str = Field(description="Tipo de amenaza (brute_force, privilege_escalation, etc.)") + severity: str = Field(description="Severidad: low, medium, high, critical") + description: str = Field(description="Descripción de la amenaza") + occurrences: int = Field(description="Número de ocurrencias") + affected_ips: list[str] = Field(default=[], description="IPs involucradas") + affected_users: list[str] = Field(default=[], description="Usuarios afectados") + first_seen: datetime = Field(description="Primera ocurrencia") + last_seen: datetime = Field(description="Última ocurrencia") + recommendations: list[str] = Field(default=[], description="Recomendaciones de acción") + + +class SecurityAnalysisResponse(BaseModel): + """Análisis completo de seguridad.""" + overall_risk_level: str = Field(description="Nivel de riesgo general: safe, low, medium, high, critical") + total_threats_detected: int = Field(description="Total de amenazas detectadas") + threats: list[SecurityThreatPattern] = Field(description="Lista de amenazas detectadas") + analysis_period_hours: int = Field(description="Período de análisis en horas") + generated_at: datetime = Field(description="Timestamp del análisis") + + # Estadísticas de seguridad + failed_login_attempts: int = Field(description="Intentos fallidos de login") + suspicious_ips_count: int = Field(description="IPs sospechosas detectadas") + critical_actions_count: int = Field(description="Acciones críticas realizadas") + + # Opciones de acción + recommended_actions: list[str] = Field(default=[], description="Acciones recomendadas") + + +class SecurityActionRequest(BaseModel): + """Solicitud de acción de seguridad.""" + action_type: str = Field(description="Tipo de acción: block_ip, notify_admin, reset_password, etc.") + target: str = Field(description="Objetivo de la acción (IP, email, etc.)") + reason: str = Field(description="Razón de la acción") + duration_minutes: Optional[int] = Field(None, description="Duración del bloqueo en minutos") + + +class SecurityActionResponse(BaseModel): + """Respuesta de acción de seguridad.""" + success: bool = Field(description="Si la acción fue exitosa") + message: str = Field(description="Mensaje descriptivo") + action_id: Optional[UUID4] = Field(None, description="ID de la acción registrada") + + class AuditLogFilters(BaseModel): """ Filtros para consulta de audit logs. diff --git a/backend/app/api/v1/endpoints/audit.py b/backend/app/api/v1/endpoints/audit.py index f963e3c..420ff97 100644 --- a/backend/app/api/v1/endpoints/audit.py +++ b/backend/app/api/v1/endpoints/audit.py @@ -19,11 +19,16 @@ from app.api.deps import get_current_user, get_current_tenant from app.models.user import User, UserRole from app.models.tenant import Tenant from app.models.audit import AuditLog +from app.services.audit_service import AuditService from app.api.schemas.audit import ( AuditLogResponse, AuditLogListResponse, AuditLogFilters, - AuditLogStats + AuditLogStats, + SecurityAnalysisResponse, + SecurityThreatPattern, + SecurityActionRequest, + SecurityActionResponse ) router = APIRouter() @@ -390,3 +395,321 @@ async def get_audit_log_detail( log_dict["user_name"] = log.user.full_name return AuditLogResponse(**log_dict) + + +# =================================== +# SECURITY ANALYSIS ENDPOINTS +# =================================== + +@router.get("/security/analysis", response_model=SecurityAnalysisResponse) +async def get_security_analysis( + hours: int = Query(default=24, ge=1, le=168, description="Período de análisis en horas"), + current_user: User = Depends(require_auditor_role), + current_tenant: Tenant = Depends(get_current_tenant), + db: AsyncSession = Depends(get_db) +): + """ + Análisis de seguridad y detección de amenazas. + + **Permisos**: ADMIN, SUPPORT_MANAGER, AUDITOR + + **Detecta**: + - Intentos de fuerza bruta (login_failed) + - Escalada de privilegios + - Eliminaciones masivas + - Accesos desde IPs sospechosas + - Patrones anómalos de actividad + + **Retorna**: Análisis completo con amenazas y recomendaciones + """ + logger.info( + "Security analysis requested", + user_id=str(current_user.id), + tenant_id=str(current_tenant.id), + hours=hours + ) + + now = datetime.utcnow() + analysis_start = now - timedelta(hours=hours) + + threats = [] + failed_login_attempts = 0 + suspicious_ips = set() + critical_actions_count = 0 + + # 1. DETECCIÓN DE FUERZA BRUTA + brute_force_query = select( + AuditLog.ip_address, + func.count(AuditLog.id).label('attempts'), + func.min(AuditLog.created_at).label('first_seen'), + func.max(AuditLog.created_at).label('last_seen') + ).where( + and_( + AuditLog.tenant_id == current_tenant.id, + AuditLog.action == 'user.login_failed', + AuditLog.created_at >= analysis_start + ) + ).group_by(AuditLog.ip_address).having(func.count(AuditLog.id) >= 5) + + brute_force_result = await db.execute(brute_force_query) + brute_force_ips = brute_force_result.all() + + for ip_data in brute_force_ips: + if ip_data.ip_address: + suspicious_ips.add(str(ip_data.ip_address)) + failed_login_attempts += ip_data.attempts + + severity = "high" if ip_data.attempts > 20 else "medium" if ip_data.attempts > 10 else "low" + + threats.append(SecurityThreatPattern( + type="brute_force_attack", + severity=severity, + description=f"Ataque de fuerza bruta detectado desde {ip_data.ip_address}", + occurrences=ip_data.attempts, + affected_ips=[str(ip_data.ip_address)], + affected_users=[], + first_seen=ip_data.first_seen, + last_seen=ip_data.last_seen, + recommendations=[ + f"Bloquear IP {ip_data.ip_address} temporalmente", + "Revisar logs de firewall", + "Considerar implementar CAPTCHA", + "Notificar al equipo de seguridad" + ] + )) + + # 2. ESCALADA DE PRIVILEGIOS + privilege_query = select( + User.email, + func.count(AuditLog.id).label('changes'), + func.min(AuditLog.created_at).label('first_seen'), + func.max(AuditLog.created_at).label('last_seen') + ).join( + User, AuditLog.user_id == User.id + ).where( + and_( + AuditLog.tenant_id == current_tenant.id, + AuditLog.action == 'user.update', + AuditLog.created_at >= analysis_start, + AuditLog.new_values.contains('"role"') + ) + ).group_by(User.email).having(func.count(AuditLog.id) >= 3) + + privilege_result = await db.execute(privilege_query) + privilege_changes = privilege_result.all() + + for priv_data in privilege_changes: + threats.append(SecurityThreatPattern( + type="privilege_escalation", + severity="critical", + description=f"Posible escalada de privilegios - {priv_data.email} ha modificado roles {priv_data.changes} veces", + occurrences=priv_data.changes, + affected_ips=[], + affected_users=[priv_data.email], + first_seen=priv_data.first_seen, + last_seen=priv_data.last_seen, + recommendations=[ + f"Revisar permisos del usuario {priv_data.email}", + "Auditar todos los cambios de roles realizados", + "Verificar si los cambios fueron autorizados", + "Considerar revertir cambios no autorizados" + ] + )) + + # 3. ELIMINACIONES MASIVAS + deletion_query = select( + User.email, + func.count(AuditLog.id).label('deletions'), + func.min(AuditLog.created_at).label('first_seen'), + func.max(AuditLog.created_at).label('last_seen') + ).join( + User, AuditLog.user_id == User.id + ).where( + and_( + AuditLog.tenant_id == current_tenant.id, + AuditLog.action.like('%.delete'), + AuditLog.created_at >= analysis_start + ) + ).group_by(User.email).having(func.count(AuditLog.id) >= 10) + + deletion_result = await db.execute(deletion_query) + mass_deletions = deletion_result.all() + + for del_data in mass_deletions: + critical_actions_count += del_data.deletions + threats.append(SecurityThreatPattern( + type="mass_deletion", + severity="high", + description=f"Eliminaciones masivas detectadas - {del_data.email} ha eliminado {del_data.deletions} recursos", + occurrences=del_data.deletions, + affected_ips=[], + affected_users=[del_data.email], + first_seen=del_data.first_seen, + last_seen=del_data.last_seen, + recommendations=[ + f"Verificar urgentemente las eliminaciones de {del_data.email}", + "Comprobar si hay backups disponibles", + "Contactar al usuario para verificar la acción", + "Revisar sistema de permisos" + ] + )) + + # 4. ACCESOS DESDE MÚLTIPLES IPS (Cuenta comprometida) + multi_ip_query = select( + User.email, + func.count(func.distinct(AuditLog.ip_address)).label('ip_count'), + func.min(AuditLog.created_at).label('first_seen'), + func.max(AuditLog.created_at).label('last_seen') + ).join( + User, AuditLog.user_id == User.id + ).where( + and_( + AuditLog.tenant_id == current_tenant.id, + AuditLog.action.in_(['user.login', 'user.logout']), + AuditLog.created_at >= analysis_start + ) + ).group_by(User.email).having(func.count(func.distinct(AuditLog.ip_address)) >= 5) + + multi_ip_result = await db.execute(multi_ip_query) + multi_ip_users = multi_ip_result.all() + + for ip_data in multi_ip_users: + threats.append(SecurityThreatPattern( + type="account_compromise", + severity="medium", + description=f"Posible cuenta comprometida - {ip_data.email} accedió desde {ip_data.ip_count} IPs diferentes", + occurrences=ip_data.ip_count, + affected_ips=[], + affected_users=[ip_data.email], + first_seen=ip_data.first_seen, + last_seen=ip_data.last_seen, + recommendations=[ + f"Contactar a {ip_data.email} para verificar actividad", + "Forzar cambio de contraseña", + "Revisar ubicaciones de acceso", + "Considerar habilitar 2FA obligatorio" + ] + )) + + # Calcular nivel de riesgo general + critical_count = sum(1 for t in threats if t.severity == "critical") + high_count = sum(1 for t in threats if t.severity == "high") + medium_count = sum(1 for t in threats if t.severity == "medium") + + if critical_count > 0: + overall_risk = "critical" + elif high_count >= 3: + overall_risk = "high" + elif high_count > 0 or medium_count >= 3: + overall_risk = "medium" + elif medium_count > 0 or len(threats) > 0: + overall_risk = "low" + else: + overall_risk = "safe" + + # Recomendaciones generales + recommended_actions = [] + if failed_login_attempts > 20: + recommended_actions.append("Implementar límite de intentos de login por IP") + if len(suspicious_ips) > 0: + recommended_actions.append(f"Bloquear {len(suspicious_ips)} IPs sospechosas identificadas") + if critical_actions_count > 50: + recommended_actions.append("Revisar políticas de permisos - demasiadas acciones críticas") + if len(threats) == 0: + recommended_actions.append("Sistema seguro - continuar monitoreando") + + return SecurityAnalysisResponse( + overall_risk_level=overall_risk, + total_threats_detected=len(threats), + threats=threats, + analysis_period_hours=hours, + generated_at=now, + failed_login_attempts=failed_login_attempts, + suspicious_ips_count=len(suspicious_ips), + critical_actions_count=critical_actions_count, + recommended_actions=recommended_actions + ) + + +@router.post("/security/action", response_model=SecurityActionResponse) +async def execute_security_action( + action: SecurityActionRequest, + current_user: User = Depends(require_auditor_role), + current_tenant: Tenant = Depends(get_current_tenant), + db: AsyncSession = Depends(get_db) +): + """ + Ejecutar acción de seguridad. + + **Permisos**: ADMIN, SUPPORT_MANAGER (solo ellos pueden ejecutar acciones) + + **Acciones disponibles**: + - `block_ip`: Bloquear IP temporalmente + - `notify_admin`: Notificar administradores + - `force_password_reset`: Forzar cambio de contraseña + - `disable_user`: Desactivar usuario temporalmente + + **Retorna**: Resultado de la acción + """ + # Verificar que solo ADMIN y SUPPORT_MANAGER puedan ejecutar acciones + if current_user.role not in [UserRole.ADMIN, UserRole.SUPPORT_MANAGER]: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Solo administradores pueden ejecutar acciones de seguridad" + ) + + logger.info( + "Security action requested", + user_id=str(current_user.id), + action_type=action.action_type, + target=action.target + ) + + # Registrar la acción en auditoría + try: + await AuditService.log( + db=db, + tenant_id=current_tenant.id, + user_id=current_user.id, + action=f"security.{action.action_type}", + resource_type="security", + resource_id=None, + metadata={ + "target": action.target, + "reason": action.reason, + "duration_minutes": action.duration_minutes + } + ) + await db.commit() + except Exception as e: + logger.error("Failed to log security action", error=str(e)) + + # Por ahora, simular la ejecución (en producción conectar con firewall, email, etc.) + message = "" + success = True + + if action.action_type == "block_ip": + message = f"IP {action.target} bloqueada por {action.duration_minutes or 60} minutos. Razón: {action.reason}" + # TODO: Integrar con firewall/WAF + + elif action.action_type == "notify_admin": + message = f"Notificación enviada a administradores sobre: {action.reason}" + # TODO: Enviar email/Slack notification + + elif action.action_type == "force_password_reset": + message = f"Se forzará cambio de contraseña para {action.target}. Razón: {action.reason}" + # TODO: Marcar usuario para reset password + + elif action.action_type == "disable_user": + message = f"Usuario {action.target} desactivado temporalmente. Razón: {action.reason}" + # TODO: Desactivar usuario en BD + + else: + success = False + message = f"Tipo de acción no reconocida: {action.action_type}" + + return SecurityActionResponse( + success=success, + message=message, + action_id=None # TODO: Retornar ID del audit log creado + ) diff --git a/frontend-internal/src/lib/components/Sidebar.svelte b/frontend-internal/src/lib/components/Sidebar.svelte index 3e383ca..b6fd0a6 100644 --- a/frontend-internal/src/lib/components/Sidebar.svelte +++ b/frontend-internal/src/lib/components/Sidebar.svelte @@ -62,6 +62,11 @@ name: 'Auditoría', href: '/audit', icon: 'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z' + }, + { + name: 'Seguridad', + href: '/audit/security', + icon: 'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z' } ); } diff --git a/frontend-internal/src/routes/audit/security/+page.svelte b/frontend-internal/src/routes/audit/security/+page.svelte new file mode 100644 index 0000000..aa3b3e6 --- /dev/null +++ b/frontend-internal/src/routes/audit/security/+page.svelte @@ -0,0 +1,532 @@ + + +
+ Detección de amenazas y análisis de vulnerabilidades +
+Análisis de {analysis.analysis_period_hours} horas
+Generado: {formatDate(analysis.generated_at)}
+Amenazas Detectadas
+{analysis.total_threats_detected}
+Intentos Fallidos
+{analysis.failed_login_attempts}
+IPs Sospechosas
+{analysis.suspicious_ips_count}
+Acciones Críticas
+{analysis.critical_actions_count}
+{threat.description}
+{ip}
+ {/each}
+ Recomendaciones:
+No se detectaron amenazas en el período analizado
+