Feature: Sistema de Análisis de Seguridad y Detección de Vulnerabilidades v1.6.0
Nuevas funcionalidades: - Sistema completo de análisis de seguridad con detección de amenazas - Detección de patrones: fuerza bruta, escalada de privilegios, eliminaciones masivas, cuentas comprometidas - Panel de vulnerabilidades con visualización detallada - Acciones de seguridad: bloqueo de IPs, notificaciones, reset de contraseñas - Análisis configurable (24h, 48h, 7 días) Backend (/audit/security/): - GET /analysis: Análisis completo de seguridad con amenazas detectadas - POST /action: Ejecutar acciones de seguridad (solo ADMIN/SUPPORT_MANAGER) - Schemas nuevos: SecurityAnalysisResponse, SecurityThreatPattern, SecurityActionRequest Frontend (/audit/security): - Panel completo de análisis con nivel de riesgo general - Visualización de amenazas con severidad (critical, high, medium, low) - Estadísticas: amenazas, intentos fallidos, IPs sospechosas, acciones críticas - Opciones de acción por amenaza: bloquear IP, resetear contraseña, notificar admin - Modal de ejecución de acciones de seguridad Mejoras: - Sidebar actualizado con enlace 'Seguridad' - Permisos: Solo ADMIN/SUPPORT_MANAGER/AUDITOR pueden ver análisis - Solo ADMIN/SUPPORT_MANAGER pueden ejecutar acciones - Audit log de todas las acciones de seguridad ejecutadas
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user