- Corregido query SQL para detección de escalada de privilegios (JSONB operator) - Añadidas rutas de autenticación faltantes al middleware tenant - Sistema completamente verificado y funcional v1.6.0
716 lines
25 KiB
Python
716 lines
25 KiB
Python
"""
|
|
Audit Endpoints - ServiceManagerWeb
|
|
|
|
Endpoints para consulta de logs de auditoría.
|
|
Solo accesible por roles: ADMIN, SUPPORT_MANAGER, AUDITOR
|
|
"""
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select, func, and_, or_, desc
|
|
from sqlalchemy.orm import selectinload
|
|
from typing import Optional, List
|
|
from datetime import datetime, timedelta
|
|
import uuid
|
|
import structlog
|
|
|
|
from app.core.database import get_db
|
|
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,
|
|
SecurityAnalysisResponse,
|
|
SecurityThreatPattern,
|
|
SecurityActionRequest,
|
|
SecurityActionResponse
|
|
)
|
|
|
|
router = APIRouter()
|
|
logger = structlog.get_logger(__name__)
|
|
|
|
|
|
def require_auditor_role(current_user: User = Depends(get_current_user)) -> User:
|
|
"""
|
|
Dependency que verifica que el usuario tenga rol de auditor.
|
|
|
|
Solo ADMIN, SUPPORT_MANAGER y AUDITOR pueden ver logs de auditoría.
|
|
"""
|
|
allowed_roles = [UserRole.ADMIN, UserRole.SUPPORT_MANAGER, UserRole.AUDITOR]
|
|
|
|
if current_user.role not in allowed_roles:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Solo usuarios con rol ADMIN, SUPPORT_MANAGER o AUDITOR pueden acceder a logs de auditoría"
|
|
)
|
|
|
|
return current_user
|
|
|
|
|
|
@router.get("/", response_model=AuditLogListResponse)
|
|
async def get_audit_logs(
|
|
# Paginaci├│n
|
|
page: int = Query(default=1, ge=1, description="Número de página"),
|
|
per_page: int = Query(default=50, ge=1, le=100, description="Registros por página"),
|
|
|
|
# Filtros
|
|
user_id: Optional[uuid.UUID] = Query(None, description="Filtrar por usuario"),
|
|
action: Optional[str] = Query(None, description="Filtrar por acci├│n"),
|
|
resource_type: Optional[str] = Query(None, description="Filtrar por tipo de recurso"),
|
|
resource_id: Optional[uuid.UUID] = Query(None, description="Filtrar por ID de recurso"),
|
|
date_from: Optional[datetime] = Query(None, description="Fecha desde"),
|
|
date_to: Optional[datetime] = Query(None, description="Fecha hasta"),
|
|
search: Optional[str] = Query(None, description="B├║squeda en acci├│n o email"),
|
|
# Multi-tenant filters (solo ADMIN/SUPPORT_MANAGER)
|
|
tenant_id: Optional[uuid.UUID] = Query(None, description="Ver logs de un tenant específico"),
|
|
all_tenants: bool = Query(False, description="Ver logs de todos los tenants"),
|
|
# Dependencies
|
|
current_user: User = Depends(require_auditor_role),
|
|
current_tenant: Tenant = Depends(get_current_tenant),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
Obtener logs de auditoría con filtros y paginación.
|
|
|
|
**Permisos**: ADMIN, SUPPORT_MANAGER, AUDITOR
|
|
|
|
**Filtros disponibles**:
|
|
- `user_id`: Acciones de un usuario específico
|
|
- `action`: Tipo de acci├│n (ej: "ticket.create")
|
|
- `resource_type`: Tipo de recurso (ej: "ticket")
|
|
- `resource_id`: ID de recurso específico
|
|
- `date_from`, `date_to`: Rango de fechas
|
|
- `search`: B├║squeda en acciones
|
|
|
|
**Retorna**: Lista paginada de audit logs
|
|
"""
|
|
logger.info(
|
|
"Fetching audit logs",
|
|
user_id=str(current_user.id),
|
|
tenant_id=str(current_tenant.id),
|
|
filters={
|
|
"user_id": str(user_id) if user_id else None,
|
|
"action": action,
|
|
"resource_type": resource_type,
|
|
"page": page,
|
|
"tenant_filter": str(tenant_id) if tenant_id else None,
|
|
"all_tenants": all_tenants
|
|
}
|
|
)
|
|
|
|
# Determinar el filtro de tenant
|
|
# Solo ADMIN y SUPPORT_MANAGER pueden ver otros tenants o todos los tenants
|
|
can_see_all_tenants = current_user.role in [UserRole.ADMIN, UserRole.SUPPORT_MANAGER]
|
|
|
|
# Query base con filtro de tenant dinámico
|
|
query = select(AuditLog).options(selectinload(AuditLog.user))
|
|
|
|
if all_tenants and can_see_all_tenants:
|
|
# Ver todos los tenants (no agregar filtro de tenant)
|
|
pass
|
|
elif tenant_id and can_see_all_tenants:
|
|
# Ver un tenant específico
|
|
query = query.where(AuditLog.tenant_id == tenant_id)
|
|
else:
|
|
# Ver solo el tenant actual (comportamiento default)
|
|
query = query.where(AuditLog.tenant_id == current_tenant.id)
|
|
|
|
# Aplicar filtros
|
|
if user_id:
|
|
query = query.where(AuditLog.user_id == user_id)
|
|
|
|
if action:
|
|
query = query.where(AuditLog.action == action)
|
|
|
|
if resource_type:
|
|
query = query.where(AuditLog.resource_type == resource_type)
|
|
|
|
if resource_id:
|
|
query = query.where(AuditLog.resource_id == resource_id)
|
|
|
|
if date_from:
|
|
query = query.where(AuditLog.created_at >= date_from)
|
|
|
|
if date_to:
|
|
# El frontend ya envía el timestamp correcto
|
|
query = query.where(AuditLog.created_at < date_to)
|
|
|
|
if search:
|
|
# B├║squeda en action
|
|
search_filter = AuditLog.action.ilike(f"%{search}%")
|
|
query = query.where(search_filter)
|
|
|
|
# Ordenar por fecha descendente (más recientes primero)
|
|
query = query.order_by(desc(AuditLog.created_at))
|
|
|
|
# Contar total antes de paginar
|
|
count_query = select(func.count()).select_from(query.subquery())
|
|
total_result = await db.execute(count_query)
|
|
total = total_result.scalar() or 0
|
|
|
|
# Aplicar paginaci├│n
|
|
offset = (page - 1) * per_page
|
|
query = query.offset(offset).limit(per_page)
|
|
|
|
# Ejecutar query
|
|
result = await db.execute(query)
|
|
logs = result.scalars().all()
|
|
|
|
# Calcular total de páginas
|
|
total_pages = (total + per_page - 1) // per_page
|
|
|
|
# Convertir a response schema (agregar info del usuario)
|
|
logs_response = []
|
|
for log in logs:
|
|
log_dict = {
|
|
"id": log.id,
|
|
"tenant_id": log.tenant_id,
|
|
"user_id": log.user_id,
|
|
"action": log.action,
|
|
"resource_type": log.resource_type,
|
|
"resource_id": log.resource_id,
|
|
"ip_address": str(log.ip_address) if log.ip_address else None,
|
|
"user_agent": log.user_agent,
|
|
"correlation_id": log.correlation_id,
|
|
"old_values": log.old_values,
|
|
"new_values": log.new_values,
|
|
"metadata": log.extra_metadata,
|
|
"created_at": log.created_at,
|
|
"action_display": log.action_display,
|
|
"user_email": None,
|
|
"user_name": None
|
|
}
|
|
|
|
# Agregar info del usuario si existe
|
|
if log.user:
|
|
log_dict["user_email"] = log.user.email
|
|
log_dict["user_name"] = log.user.full_name
|
|
log_dict["user_role"] = log.user.role.value if hasattr(log.user.role, 'value') else str(log.user.role)
|
|
|
|
logs_response.append(AuditLogResponse(**log_dict))
|
|
|
|
return AuditLogListResponse(
|
|
logs=logs_response,
|
|
total=total,
|
|
page=page,
|
|
per_page=per_page,
|
|
total_pages=total_pages
|
|
)
|
|
|
|
|
|
@router.get("/stats", response_model=AuditLogStats)
|
|
async def get_audit_stats(
|
|
all_tenants: bool = Query(False, description="Ver stats de todos los tenants"),
|
|
current_user: User = Depends(require_auditor_role),
|
|
current_tenant: Tenant = Depends(get_current_tenant),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
Obtener estadísticas de auditoría del tenant (o todos los tenants si es ADMIN).
|
|
|
|
**Permisos**: ADMIN, SUPPORT_MANAGER, AUDITOR
|
|
|
|
**Retorna**: Estadísticas de actividad
|
|
"""
|
|
can_see_all_tenants = current_user.role in [UserRole.ADMIN, UserRole.SUPPORT_MANAGER]
|
|
|
|
logger.info(
|
|
"Fetching audit stats",
|
|
user_id=str(current_user.id),
|
|
tenant_id=str(current_tenant.id),
|
|
all_tenants=all_tenants,
|
|
can_see_all=can_see_all_tenants
|
|
)
|
|
|
|
now = datetime.utcnow()
|
|
|
|
# Determinar si aplicar filtro de tenant
|
|
apply_tenant_filter = not (all_tenants and can_see_all_tenants)
|
|
|
|
# Total de acciones
|
|
total_query = select(func.count()).select_from(AuditLog)
|
|
if apply_tenant_filter:
|
|
total_query = total_query.where(AuditLog.tenant_id == current_tenant.id)
|
|
total_result = await db.execute(total_query)
|
|
total_actions = total_result.scalar() or 0
|
|
|
|
# Acciones hoy (├║ltimas 24 horas)
|
|
today_start = now - timedelta(days=1)
|
|
today_query = select(func.count()).select_from(AuditLog).where(
|
|
AuditLog.created_at >= today_start
|
|
)
|
|
if apply_tenant_filter:
|
|
today_query = today_query.where(AuditLog.tenant_id == current_tenant.id)
|
|
today_result = await db.execute(today_query)
|
|
actions_today = today_result.scalar() or 0
|
|
|
|
# Acciones esta semana (últimos 7 días)
|
|
week_start = now - timedelta(days=7)
|
|
week_query = select(func.count()).select_from(AuditLog).where(
|
|
AuditLog.created_at >= week_start
|
|
)
|
|
if apply_tenant_filter:
|
|
week_query = week_query.where(AuditLog.tenant_id == current_tenant.id)
|
|
week_result = await db.execute(week_query)
|
|
actions_this_week = week_result.scalar() or 0
|
|
|
|
# Top 5 acciones más frecuentes
|
|
top_actions_query = select(
|
|
AuditLog.action,
|
|
func.count(AuditLog.id).label('count')
|
|
)
|
|
if apply_tenant_filter:
|
|
top_actions_query = top_actions_query.where(AuditLog.tenant_id == current_tenant.id)
|
|
top_actions_query = top_actions_query.group_by(
|
|
AuditLog.action
|
|
).order_by(
|
|
desc('count')
|
|
).limit(5)
|
|
|
|
top_actions_result = await db.execute(top_actions_query)
|
|
top_actions = {row.action: row.count for row in top_actions_result}
|
|
|
|
# Acciones por tipo de recurso
|
|
by_resource_query = select(
|
|
AuditLog.resource_type,
|
|
func.count(AuditLog.id).label('count')
|
|
)
|
|
if apply_tenant_filter:
|
|
by_resource_query = by_resource_query.where(AuditLog.tenant_id == current_tenant.id)
|
|
by_resource_query = by_resource_query.group_by(
|
|
AuditLog.resource_type
|
|
).order_by(
|
|
desc('count')
|
|
)
|
|
|
|
by_resource_result = await db.execute(by_resource_query)
|
|
by_resource_type = {row.resource_type: row.count for row in by_resource_result}
|
|
|
|
# Top usuarios (con join a users para obtener nombres)
|
|
top_users_query = select(
|
|
User.email,
|
|
func.count(AuditLog.id).label('count')
|
|
).join(
|
|
User, AuditLog.user_id == User.id
|
|
)
|
|
if apply_tenant_filter:
|
|
top_users_query = top_users_query.where(AuditLog.tenant_id == current_tenant.id)
|
|
top_users_query = top_users_query.group_by(
|
|
User.email
|
|
).order_by(
|
|
desc('count')
|
|
).limit(5)
|
|
|
|
top_users_result = await db.execute(top_users_query)
|
|
top_users = {row.email: row.count for row in top_users_result}
|
|
|
|
# Acciones críticas hoy (delete, update sensibles, etc.)
|
|
critical_conditions = [
|
|
AuditLog.created_at >= today_start,
|
|
or_(
|
|
AuditLog.action.like('%.delete'),
|
|
AuditLog.action.like('user.update'),
|
|
AuditLog.action.like('%.assign'),
|
|
AuditLog.action.in_(['user.login_failed', 'user.logout'])
|
|
)
|
|
]
|
|
if apply_tenant_filter:
|
|
critical_conditions.append(AuditLog.tenant_id == current_tenant.id)
|
|
|
|
critical_actions_query = select(func.count()).select_from(AuditLog).where(
|
|
and_(*critical_conditions)
|
|
)
|
|
critical_result = await db.execute(critical_actions_query)
|
|
critical_actions_today = critical_result.scalar() or 0
|
|
|
|
return AuditLogStats(
|
|
total_actions=total_actions,
|
|
actions_today=actions_today,
|
|
actions_this_week=actions_this_week,
|
|
critical_actions_today=critical_actions_today,
|
|
top_actions=top_actions,
|
|
top_users=top_users,
|
|
by_resource_type=by_resource_type
|
|
)
|
|
|
|
|
|
@router.get("/{log_id}", response_model=AuditLogResponse)
|
|
async def get_audit_log_detail(
|
|
log_id: uuid.UUID,
|
|
current_user: User = Depends(require_auditor_role),
|
|
current_tenant: Tenant = Depends(get_current_tenant),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
Obtener detalle de un audit log específico.
|
|
|
|
**Permisos**: ADMIN, SUPPORT_MANAGER, AUDITOR
|
|
|
|
**Retorna**: Detalle completo del audit log
|
|
"""
|
|
# Buscar el log
|
|
query = select(AuditLog).where(
|
|
and_(
|
|
AuditLog.id == log_id,
|
|
AuditLog.tenant_id == current_tenant.id
|
|
)
|
|
)
|
|
|
|
result = await db.execute(query)
|
|
log = result.scalar_one_or_none()
|
|
|
|
if not log:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"Audit log {log_id} no encontrado"
|
|
)
|
|
|
|
# Convertir a response
|
|
log_dict = {
|
|
"id": log.id,
|
|
"tenant_id": log.tenant_id,
|
|
"user_id": log.user_id,
|
|
"action": log.action,
|
|
"resource_type": log.resource_type,
|
|
"resource_id": log.resource_id,
|
|
"ip_address": str(log.ip_address) if log.ip_address else None,
|
|
"user_agent": log.user_agent,
|
|
"correlation_id": log.correlation_id,
|
|
"old_values": log.old_values,
|
|
"new_values": log.new_values,
|
|
"metadata": log.extra_metadata,
|
|
"created_at": log.created_at,
|
|
"action_display": log.action_display,
|
|
"user_email": None,
|
|
"user_name": None
|
|
}
|
|
|
|
if log.user:
|
|
log_dict["user_email"] = log.user.email
|
|
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.op('?')('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
|
|
)
|