""" ESET sync jobs """ import logging from typing import Any, Dict, List, Optional from psycopg2.extras import Json from app.core.config import settings from app.core.database import execute_query from app.services.eset_service import eset_service logger = logging.getLogger(__name__) def _extract_first_str(payload: Any, keys: List[str]) -> Optional[str]: if payload is None: return None key_set = {k.lower() for k in keys} stack = [payload] while stack: current = stack.pop() if isinstance(current, dict): for k, v in current.items(): if k.lower() in key_set and isinstance(v, str) and v.strip(): return v.strip() if isinstance(v, (dict, list)): stack.append(v) elif isinstance(current, list): for item in current: if isinstance(item, (dict, list)): stack.append(item) return None def _extract_devices(payload: Any) -> List[Dict[str, Any]]: if isinstance(payload, list): return [d for d in payload if isinstance(d, dict)] if isinstance(payload, dict): for key in ("devices", "items", "results", "data"): value = payload.get(key) if isinstance(value, list): return [d for d in value if isinstance(d, dict)] return [] def _extract_company(payload: Any) -> Optional[str]: company = _extract_first_str(payload, ["company", "organization", "tenant", "customer", "userCompany"]) if company: return company group_path = _extract_group_path(payload) if group_path and "/" in group_path: return group_path.split("/")[-1].strip() or None return None def _extract_group_path(payload: Any) -> Optional[str]: return _extract_first_str(payload, ["parentGroup", "groupPath", "group", "path"]) def _extract_group_name(payload: Any) -> Optional[str]: group_path = _extract_group_path(payload) if group_path and "/" in group_path: name = group_path.split("/")[-1].strip() return name or None return group_path def _extract_full_name(payload: Any) -> Optional[str]: name = _extract_first_str(payload, ["realName", "displayName", "userName", "owner", "user", "lastLoggedInUser"]) if name: return name first = _extract_first_str(payload, ["firstName", "givenName"]) last = _extract_first_str(payload, ["lastName", "surname", "familyName"]) if first and last: return f"{first} {last}".strip() return None def _detect_asset_type(payload: Any) -> str: device_type = _extract_first_str(payload, ["deviceType", "type"]) if device_type: val = device_type.lower() if "server" in val: return "server" if "laptop" in val or "notebook" in val: return "laptop" return "pc" def _match_contact(full_name: str, company: str) -> Optional[int]: query = """ SELECT id FROM contacts WHERE LOWER(TRIM(first_name || ' ' || last_name)) = LOWER(%s) AND LOWER(COALESCE(user_company, '')) = LOWER(%s) LIMIT 1 """ result = execute_query(query, (full_name, company)) if result: return result[0]["id"] return None def _get_contact_customer(contact_id: int) -> Optional[int]: query = """ SELECT customer_id FROM contact_companies WHERE contact_id = %s ORDER BY is_primary DESC, id ASC LIMIT 1 """ result = execute_query(query, (contact_id,)) if result: return result[0]["customer_id"] return None def _match_customer_exact(name: str) -> Optional[int]: if not name: return None query = "SELECT id FROM customers WHERE LOWER(name) = LOWER(%s)" result = execute_query(query, (name,)) if len(result or []) == 1: return result[0]["id"] return None def _upsert_hardware_contact(hardware_id: int, contact_id: int) -> None: query = """ INSERT INTO hardware_contacts (hardware_id, contact_id, role, source) VALUES (%s, %s, %s, %s) ON CONFLICT (hardware_id, contact_id) DO NOTHING """ execute_query(query, (hardware_id, contact_id, "primary", "eset")) def _upsert_incident(incident: Dict[str, Any]) -> None: incident_uuid = incident.get("incidentUuid") or incident.get("uuid") or incident.get("id") if not incident_uuid: return severity = incident.get("severity") or incident.get("level") status = incident.get("status") device_uuid = incident.get("deviceUuid") or incident.get("device") detected_at = incident.get("detectedAt") or incident.get("firstSeen") last_seen = incident.get("lastSeen") or incident.get("lastUpdate") query = """ INSERT INTO eset_incidents ( incident_uuid, severity, status, device_uuid, detected_at, last_seen, payload, updated_at ) VALUES (%s, %s, %s, %s, %s, %s, %s, NOW()) ON CONFLICT (incident_uuid) DO UPDATE SET severity = EXCLUDED.severity, status = EXCLUDED.status, device_uuid = EXCLUDED.device_uuid, detected_at = EXCLUDED.detected_at, last_seen = EXCLUDED.last_seen, payload = EXCLUDED.payload, updated_at = NOW() """ execute_query(query, ( incident_uuid, severity, status, device_uuid, detected_at, last_seen, Json(incident) )) async def sync_eset_hardware() -> None: if not settings.ESET_ENABLED or not settings.ESET_SYNC_ENABLED: return payload = await eset_service.list_devices() if not payload: logger.warning("ESET device list empty") return devices = _extract_devices(payload) logger.info("ESET devices fetched: %d", len(devices)) for device in devices: device_uuid = device.get("deviceUuid") or device.get("uuid") or device.get("id") if not device_uuid: continue details = await eset_service.get_device_details(device_uuid) if not details: continue serial = _extract_first_str(details, ["serialNumber", "serial", "serial_number"]) model = _extract_first_str(details, ["model", "deviceModel", "deviceName", "name"]) brand = _extract_first_str(details, ["manufacturer", "brand", "vendor"]) group_path = _extract_group_path(details) group_name = _extract_group_name(details) conditions = [] params = [] conditions.append("eset_uuid = %s") params.append(device_uuid) if serial: conditions.append("serial_number = %s") params.append(serial) lookup_query = f"SELECT * FROM hardware_assets WHERE deleted_at IS NULL AND ({' OR '.join(conditions)})" existing = execute_query(lookup_query, tuple(params)) full_name = _extract_full_name(details) company = _extract_company(details) contact_id = _match_contact(full_name, company) if full_name and company else None customer_id = _get_contact_customer(contact_id) if contact_id else None if not customer_id: customer_id = _match_customer_exact(group_name or company) if (group_name or company) else None if existing: hardware_id = existing[0]["id"] update_fields = ["eset_uuid = %s", "hardware_specs = %s", "updated_at = NOW()"] update_params = [device_uuid, Json(details)] if group_path: update_fields.append("eset_group = %s") update_params.append(group_path) if not existing[0].get("serial_number") and serial: update_fields.append("serial_number = %s") update_params.append(serial) if not existing[0].get("model") and model: update_fields.append("model = %s") update_params.append(model) if not existing[0].get("brand") and brand: update_fields.append("brand = %s") update_params.append(brand) update_params.append(hardware_id) update_query = f""" UPDATE hardware_assets SET {', '.join(update_fields)} WHERE id = %s """ execute_query(update_query, tuple(update_params)) else: owner_type = "customer" if customer_id else "bmc" insert_query = """ INSERT INTO hardware_assets ( asset_type, brand, model, serial_number, current_owner_type, current_owner_customer_id, notes, eset_uuid, hardware_specs, eset_group ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id """ insert_params = ( _detect_asset_type(details), brand, model, serial, owner_type, customer_id, "Auto-created from ESET", device_uuid, Json(details), group_path ) created = execute_query(insert_query, insert_params) hardware_id = created[0]["id"] if created else None if contact_id and hardware_id: _upsert_hardware_contact(hardware_id, contact_id) if customer_id: owner_query = """ UPDATE hardware_assets SET current_owner_type = %s, current_owner_customer_id = %s, updated_at = NOW() WHERE id = %s """ execute_query(owner_query, ("customer", customer_id, hardware_id)) async def sync_eset_incidents() -> None: if not settings.ESET_ENABLED or not settings.ESET_INCIDENTS_ENABLED: return payload = await eset_service.list_incidents() if not payload: logger.warning("ESET incidents list empty") return incidents = _extract_devices(payload) critical = 0 for incident in incidents: _upsert_incident(incident) severity = (incident.get("severity") or incident.get("level") or "").lower() if severity in {"critical", "high", "severe"}: critical += 1 if critical: logger.warning("ESET critical incidents: %d", critical) async def run_eset_sync() -> None: await sync_eset_hardware() await sync_eset_incidents()