"""Helper functions for audit endpoints""" from sqlalchemy import select, func, and_, or_, desc from sqlalchemy.ext.asyncio import AsyncSession from typing import Optional, Dict, List from datetime import datetime import uuid from app.models.audit import AuditLog from app.models.user import User, UserRole from app.models.tenant import Tenant def audit_log_to_dict(log: AuditLog) -> dict: """Convierte AuditLog a diccionario de respuesta""" 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 log_dict["user_role"] = log.user.role.value if hasattr(log.user.role, 'value') else str(log.user.role) return log_dict def apply_tenant_filter(query, current_user: User, current_tenant: Tenant, all_tenants: bool = False, specific_tenant_id: Optional[uuid.UUID] = None): """Aplica filtro de tenant según permisos del usuario""" can_see_all_tenants = current_user.role in [UserRole.ADMIN, UserRole.SUPPORT_MANAGER] if all_tenants and can_see_all_tenants: return query # No filtrar por tenant elif specific_tenant_id and can_see_all_tenants: return query.where(AuditLog.tenant_id == specific_tenant_id) else: return query.where(AuditLog.tenant_id == current_tenant.id) async def get_count_stat(db: AsyncSession, tenant_id: Optional[uuid.UUID] = None, date_from: Optional[datetime] = None, action_filter=None) -> int: """Obtiene estadística de conteo con filtros opcionales""" query = select(func.count()).select_from(AuditLog) if tenant_id: query = query.where(AuditLog.tenant_id == tenant_id) if date_from: query = query.where(AuditLog.created_at >= date_from) if action_filter is not None: query = query.where(action_filter) result = await db.execute(query) return result.scalar() or 0 async def get_top_items(db: AsyncSession, field, tenant_id: Optional[uuid.UUID] = None, limit: int = 5, join_user: bool = False) -> Dict[str, int]: """Obtiene top items por campo con conteo""" if join_user: query = select(User.email, func.count(AuditLog.id).label('count')).join(User, AuditLog.user_id == User.id) else: query = select(field, func.count(AuditLog.id).label('count')) if tenant_id: query = query.where(AuditLog.tenant_id == tenant_id) if not join_user: query = query.group_by(field) else: query = query.group_by(User.email) query = query.order_by(desc('count')).limit(limit) result = await db.execute(query) return {row[0]: row[1] for row in result} def detect_mass_deletions(logs: List[AuditLog], now: datetime) -> List[dict]: """Detecta eliminaciones masivas de logs de auditoría""" deletion_groups = {} for log in logs: if not log.user: continue key = f"{log.user.email}_{log.created_at.date()}" if key not in deletion_groups: deletion_groups[key] = { 'user': log.user.email, 'date': log.created_at.date(), 'count': 0, 'logs': [], 'first_seen': log.created_at, 'last_seen': log.created_at } deletion_groups[key]['count'] += 1 deletion_groups[key]['logs'].append(log) deletion_groups[key]['first_seen'] = min(deletion_groups[key]['first_seen'], log.created_at) deletion_groups[key]['last_seen'] = max(deletion_groups[key]['last_seen'], log.created_at) incidents = [] for key, group in deletion_groups.items(): if group['count'] >= 3: severity = "critical" if group['count'] >= 10 else "high" if group['count'] >= 5 else "medium" status = "active" if (now - group['last_seen']).days <= 1 else "resolved" incidents.append({ "id": f"mass_del_{key.replace('_', '-')}", "title": f"Eliminaciones masivas - {group['user']}", "description": f"{group['user']} eliminó {group['count']} elementos el {group['date']}", "severity": severity, "status": status, "incident_type": "mass_deletion", "affected_user": group['user'], "source_ip": group['logs'][0].ip_address, "evidence": [f"{log.action} - {log.resource_type} - {log.created_at.strftime('%H:%M:%S')}" for log in group['logs'][:5]], "metadata": { "total_deletions": group['count'], "resource_types": list(set(log.resource_type for log in group['logs'])), "time_span_minutes": int((group['last_seen'] - group['first_seen']).total_seconds() / 60) }, "created_at": group['first_seen'], "updated_at": group['last_seen'] }) return incidents def detect_brute_force(logs: List[AuditLog], now: datetime) -> List[dict]: """Detecta ataques de fuerza bruta de logs de login fallido""" ip_groups = {} for log in logs: if not log.ip_address: continue ip = str(log.ip_address) if ip not in ip_groups: ip_groups[ip] = {'count': 0, 'logs': [], 'first_seen': log.created_at, 'last_seen': log.created_at, 'users': set()} ip_groups[ip]['count'] += 1 ip_groups[ip]['logs'].append(log) ip_groups[ip]['first_seen'] = min(ip_groups[ip]['first_seen'], log.created_at) ip_groups[ip]['last_seen'] = max(ip_groups[ip]['last_seen'], log.created_at) if log.user and log.user.email: ip_groups[ip]['users'].add(log.user.email) incidents = [] for ip, group in ip_groups.items(): if group['count'] >= 5: severity = "critical" if group['count'] >= 20 else "high" if group['count'] >= 10 else "medium" status = "active" if (now - group['last_seen']).total_seconds() <= 86400 else "investigating" incidents.append({ "id": f"brute_force_{ip.replace('.', '-')}", "title": f"Posible ataque de fuerza bruta desde {ip}", "description": f"Se detectaron {group['count']} intentos fallidos de login desde la IP {ip}", "severity": severity, "status": status, "incident_type": "brute_force_attack", "affected_user": ', '.join(list(group['users'])[:3]) if group['users'] else None, "source_ip": ip, "evidence": [f"Login fallido - {log.user.email if log.user else 'Unknown'} - {log.created_at.strftime('%H:%M:%S')}" for log in group['logs'][:5]], "metadata": { "total_attempts": group['count'], "targeted_users": list(group['users']), "time_span_hours": int((group['last_seen'] - group['first_seen']).total_seconds() / 3600) }, "created_at": group['first_seen'], "updated_at": group['last_seen'] }) return incidents def detect_privilege_escalation(logs: List[AuditLog]) -> List[dict]: """Detecta escaladas de privilegios""" role_hierarchy = {'CLIENT_USER': 1, 'CLIENT_ADMIN': 2, 'AGENT': 3, 'SUPPORT_MANAGER': 4, 'ADMIN': 5} incidents = [] for log in logs: if not log.user or not log.new_values or 'role' not in log.new_values: continue old_role = log.old_values.get('role') if log.old_values else 'Unknown' new_role = log.new_values.get('role') old_level = role_hierarchy.get(old_role, 0) new_level = role_hierarchy.get(new_role, 0) if new_level > old_level: incidents.append({ "id": f"priv_esc_{log.id}", "title": f"Escalada de privilegios - {log.user.email}", "description": f"Usuario {log.user.email} cambió de rol {old_role} a {new_role}", "severity": "high" if new_role in ['ADMIN', 'SUPPORT_MANAGER'] else "medium", "status": "investigating", "incident_type": "privilege_escalation", "affected_user": log.user.email, "source_ip": log.ip_address, "evidence": [f"Cambio de rol: {old_role} → {new_role} - {log.created_at.strftime('%Y-%m-%d %H:%M')}"], "metadata": { "old_role": old_role, "new_role": new_role, "correlation_id": str(log.correlation_id) if log.correlation_id else None }, "created_at": log.created_at, "updated_at": log.created_at }) return incidents