"""Audit Endpoints - ServiceManagerWeb""" 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, timezone 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, SecurityIncidentResponse, SecurityIncidentListResponse ) from app.api.v1.audit_helpers import ( audit_log_to_dict, apply_tenant_filter, get_count_stat, get_top_items, detect_mass_deletions, detect_brute_force, detect_privilege_escalation ) router = APIRouter() logger = structlog.get_logger(__name__) def require_auditor_role(current_user: User = Depends(get_current_user)) -> User: """Verifica que el usuario tenga rol de auditor""" if current_user.role not in [UserRole.ADMIN, UserRole.SUPPORT_MANAGER, UserRole.AUDITOR]: 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(page: int = Query(default=1, ge=1), per_page: int = Query(default=50, ge=1, le=100), user_id: Optional[uuid.UUID] = Query(None), action: Optional[str] = Query(None), resource_type: Optional[str] = Query(None), resource_id: Optional[uuid.UUID] = Query(None), date_from: Optional[datetime] = Query(None), date_to: Optional[datetime] = Query(None), search: Optional[str] = Query(None), tenant_id: Optional[uuid.UUID] = Query(None), all_tenants: bool = Query(False), 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""" 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, "page": page, "all_tenants": all_tenants}) query = select(AuditLog).options(selectinload(AuditLog.user)) query = apply_tenant_filter(query, current_user, current_tenant, all_tenants, tenant_id) 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: query = query.where(AuditLog.created_at < date_to) if search: query = query.where(AuditLog.action.ilike(f"%{search}%")) query = query.order_by(desc(AuditLog.created_at)) count_query = select(func.count()).select_from(query.subquery()) total = (await db.execute(count_query)).scalar() or 0 offset = (page - 1) * per_page query = query.offset(offset).limit(per_page) result = await db.execute(query) logs = result.scalars().all() total_pages = (total + per_page - 1) // per_page logs_response = [AuditLogResponse(**audit_log_to_dict(log)) for log in logs] 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), 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""" 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.now(timezone.utc) apply_tenant = not (all_tenants and can_see_all_tenants) tenant_filter = current_tenant.id if apply_tenant else None total_actions = await get_count_stat(db, tenant_filter) actions_today = await get_count_stat(db, tenant_filter, now - timedelta(days=1)) actions_this_week = await get_count_stat(db, tenant_filter, now - timedelta(days=7)) today_start = now - timedelta(days=1) 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: critical_conditions.append(AuditLog.tenant_id == tenant_filter) critical_actions_today = (await db.execute(select(func.count()).select_from(AuditLog).where(and_(*critical_conditions)))).scalar() or 0 top_actions = await get_top_items(db, AuditLog.action, tenant_filter) by_resource_type = await get_top_items(db, AuditLog.resource_type, tenant_filter, limit=10) top_users = await get_top_items(db, None, tenant_filter, join_user=True) 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 log de auditoría""" query = select(AuditLog).where(AuditLog.id == log_id).options(selectinload(AuditLog.user)) query = apply_tenant_filter(query, current_user, current_tenant) 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} not found") return AuditLogResponse(**audit_log_to_dict(log)) @router.get("/security/analysis", response_model=SecurityAnalysisResponse) async def get_security_analysis(all_tenants: bool = Query(False), current_user: User = Depends(require_auditor_role), current_tenant: Tenant = Depends(get_current_tenant), db: AsyncSession = Depends(get_db)): """Análisis de seguridad basado en logs de auditoría""" logger.info("Security analysis requested", user_id=str(current_user.id), tenant_id=str(current_tenant.id)) now = datetime.now(timezone.utc) analysis_start = now - timedelta(hours=24) query = select(AuditLog).where(AuditLog.created_at >= analysis_start).options(selectinload(AuditLog.user)) query = apply_tenant_filter(query, current_user, current_tenant, all_tenants) result = await db.execute(query) logs = result.scalars().all() failed_logins = sum(1 for log in logs if log.action == 'user.login_failed') mass_deletions = sum(1 for log in logs if '.delete' in log.action) privilege_changes = sum(1 for log in logs if log.action == 'user.update' and log.new_values and 'role' in log.new_values) threat_patterns = [] if failed_logins >= 5: affected_ips_list = [str(log.ip_address) for log in logs if log.action == 'user.login_failed' and log.ip_address] threat_patterns.append(SecurityThreatPattern( id="brute_force_attempt", type="brute_force", description=f"Se detectaron {failed_logins} intentos fallidos de login en las últimas 24h", severity="high" if failed_logins >= 20 else "medium", occurrences=failed_logins, first_seen=min((log.created_at for log in logs if log.action == 'user.login_failed'), default=now), last_seen=max((log.created_at for log in logs if log.action == 'user.login_failed'), default=now), affected_ips=list(set(affected_ips_list))[:5], affected_users=[], recommended_action="Considerar bloquear IPs con múltiples fallos" )) if mass_deletions >= 10: deleting_users = [log.user.email for log in logs if '.delete' in log.action and log.user] threat_patterns.append(SecurityThreatPattern( id="mass_deletion", type="mass_deletion", description=f"Se detectaron {mass_deletions} eliminaciones en las últimas 24h", severity="critical" if mass_deletions >= 50 else "high", occurrences=mass_deletions, first_seen=min((log.created_at for log in logs if '.delete' in log.action), default=now), last_seen=max((log.created_at for log in logs if '.delete' in log.action), default=now), affected_ips=[], affected_users=list(set(deleting_users))[:5], recommended_action="Revisar qué usuarios están eliminando recursos" )) if privilege_changes >= 3: affected_users_list = [log.user.email for log in logs if log.action == 'user.update' and log.user and log.new_values and 'role' in log.new_values] threat_patterns.append(SecurityThreatPattern( id="suspicious_privilege_changes", type="privilege_escalation", description=f"Se detectaron {privilege_changes} cambios de privilegios en las últimas 24h", severity="high", occurrences=privilege_changes, first_seen=min((log.created_at for log in logs if log.action == 'user.update' and log.new_values and 'role' in log.new_values), default=now), last_seen=max((log.created_at for log in logs if log.action == 'user.update' and log.new_values and 'role' in log.new_values), default=now), affected_ips=[], affected_users=list(set(affected_users_list))[:5], recommended_action="Auditar cambios de roles recientes" )) risk_score = min(100, (failed_logins * 2) + (mass_deletions * 5) + (privilege_changes * 10)) risk_level = "critical" if risk_score >= 80 else "high" if risk_score >= 50 else "medium" if risk_score >= 20 else "low" recommended_actions = [] if failed_logins >= 20: recommended_actions.append("Implementar bloqueo automático de IPs después de múltiples intentos fallidos") if mass_deletions >= 50: recommended_actions.append("Activar confirmación adicional para eliminaciones masivas") if not recommended_actions: recommended_actions.append("Continuar monitoreando actividad del sistema") # Calcular IPs sospechosas (más de 5 intentos fallidos) suspicious_ips = len(set([log.ip_address for log in logs if log.ip_address and log.action == 'user.login_failed'])) # Contar acciones críticas (delete, privilege changes, etc) critical_actions = mass_deletions + privilege_changes return SecurityAnalysisResponse( overall_risk_level=risk_level, total_threats_detected=len(threat_patterns), threats=threat_patterns, analysis_period_hours=24, generated_at=datetime.utcnow(), failed_login_attempts=failed_logins, suspicious_ips_count=suspicious_ips, critical_actions_count=critical_actions, 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""" 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) 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)) action_messages = { "block_ip": f"IP {action.target} bloqueada por {action.duration_minutes or 60} minutos. Razón: {action.reason}", "notify_admin": f"Notificación enviada a administradores sobre: {action.reason}", "force_password_reset": f"Se forzará cambio de contraseña para {action.target}. Razón: {action.reason}", "disable_user": f"Usuario {action.target} desactivado temporalmente. Razón: {action.reason}" } success = action.action_type in action_messages message = action_messages.get(action.action_type, f"Tipo de acción no reconocida: {action.action_type}") return SecurityActionResponse(success=success, message=message, action_id=None) @router.get("/security/incidents", response_model=SecurityIncidentListResponse) async def get_security_incidents(page: int = Query(default=1, ge=1), per_page: int = Query(default=20, ge=1, le=100), severity: Optional[str] = Query(None), status: Optional[str] = Query(None), incident_type: Optional[str] = Query(None), search: Optional[str] = Query(None), all_tenants: bool = Query(False), current_user: User = Depends(require_auditor_role), current_tenant: Tenant = Depends(get_current_tenant), db: AsyncSession = Depends(get_db)): """Obtener incidentes de seguridad""" logger.info("Fetching security incidents", user_id=str(current_user.id), tenant_id=str(current_tenant.id), filters={"severity": severity, "status": status, "type": incident_type, "page": page}) now = datetime.now(timezone.utc) analysis_start = now - timedelta(days=7) base_query = select(AuditLog).options(selectinload(AuditLog.user)).where(AuditLog.created_at >= analysis_start) base_query = apply_tenant_filter(base_query, current_user, current_tenant, all_tenants) deletion_result = await db.execute(base_query.where(AuditLog.action.like('%.delete')).order_by(desc(AuditLog.created_at))) deletion_logs = deletion_result.scalars().all() deletion_incidents = detect_mass_deletions(deletion_logs, now) failed_login_result = await db.execute(base_query.where(AuditLog.action == 'user.login_failed').order_by(desc(AuditLog.created_at))) failed_login_logs = failed_login_result.scalars().all() brute_force_incidents = detect_brute_force(failed_login_logs, now) privilege_result = await db.execute(base_query.where(and_(AuditLog.action == 'user.update', AuditLog.new_values.op('?')('role'))).order_by(desc(AuditLog.created_at))) privilege_logs = privilege_result.scalars().all() privilege_incidents = detect_privilege_escalation(privilege_logs) incidents = [SecurityIncidentResponse(**inc) for inc in (deletion_incidents + brute_force_incidents + privilege_incidents)] if severity: incidents = [i for i in incidents if i.severity == severity] if status: incidents = [i for i in incidents if i.status == status] if incident_type: incidents = [i for i in incidents if i.incident_type == incident_type] if search: search_lower = search.lower() incidents = [i for i in incidents if search_lower in i.title.lower() or (i.description and search_lower in i.description.lower())] incidents.sort(key=lambda x: x.created_at, reverse=True) total = len(incidents) total_pages = (total + per_page - 1) // per_page start_idx = (page - 1) * per_page end_idx = start_idx + per_page paginated_incidents = incidents[start_idx:end_idx] return SecurityIncidentListResponse(incidents=paginated_incidents, total=total, page=page, per_page=per_page, total_pages=total_pages)