bmc_hub/app/jobs/eset_sync.py
Christian 297a8ef2d6 feat: Implement ESET integration for hardware management
- Added ESET sync functionality to periodically fetch devices and incidents.
- Created new ESET service for API interactions, including authentication and data retrieval.
- Introduced new database tables for storing ESET incidents and hardware contacts.
- Updated hardware assets schema to include ESET-specific fields (UUID, specs, group).
- Developed frontend templates for ESET overview, import, and testing.
- Enhanced existing hardware creation form to auto-generate AnyDesk links.
- Added global logout functionality to clear user session data.
- Improved error handling and logging for ESET API interactions.
2026-02-11 13:23:32 +01:00

309 lines
10 KiB
Python

"""
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()