bmc_hub/app/jobs/eset_sync.py
Christian bc504b9257 feat: Add subscription management functionality and AnyDesk API integration
- Implemented subscription creation, updating, and rendering in script_9.js.
- Added functions for handling subscription line items, product selection, and total calculations.
- Integrated AnyDesk API for session management in test_anydesk.py.
- Created REST client test requests for API endpoints in api.http.
- Developed a script to check ESET machine status and save details in tmp_check_eset_machine.py.
2026-03-30 07:50:15 +02:00

396 lines
13 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 _extract_login_candidates(payload: Any) -> List[str]:
raw = _extract_first_str(
payload,
["userPrincipalName", "upn", "email", "mail", "loginName", "login", "userName", "lastLoggedInUser"]
)
if not raw:
return []
candidates: List[str] = []
def _add(value: str) -> None:
v = (value or "").strip().lower()
if v and v not in candidates:
candidates.append(v)
_add(raw)
# DOMAIN\\user or provider/user -> user
if "\\" in raw:
_add(raw.split("\\")[-1])
if "/" in raw:
_add(raw.split("/")[-1])
# email local-part fallback
if "@" in raw:
_add(raw.split("@", 1)[0])
return candidates
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 _match_contact_by_login(login_candidate: str, company: Optional[str] = None) -> Optional[int]:
if not login_candidate:
return None
# Try scoped match first when company is known to reduce false positives.
if company:
scoped_query = """
SELECT id
FROM contacts
WHERE LOWER(COALESCE(email, '')) = LOWER(%s)
AND LOWER(COALESCE(user_company, '')) = LOWER(%s)
LIMIT 1
"""
scoped = execute_query(scoped_query, (login_candidate, company))
if scoped:
return scoped[0]["id"]
scoped_local_part_query = """
SELECT id
FROM contacts
WHERE LOWER(split_part(COALESCE(email, ''), '@', 1)) = LOWER(%s)
AND LOWER(COALESCE(user_company, '')) = LOWER(%s)
LIMIT 1
"""
scoped_local_part = execute_query(scoped_local_part_query, (login_candidate, company))
if scoped_local_part:
return scoped_local_part[0]["id"]
email_query = """
SELECT id
FROM contacts
WHERE LOWER(COALESCE(email, '')) = LOWER(%s)
LIMIT 1
"""
by_email = execute_query(email_query, (login_candidate,))
if by_email:
return by_email[0]["id"]
local_part_query = """
SELECT id
FROM contacts
WHERE LOWER(split_part(COALESCE(email, ''), '@', 1)) = LOWER(%s)
LIMIT 1
"""
by_local_part = execute_query(local_part_query, (login_candidate,))
if by_local_part:
return by_local_part[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)
login_candidates = _extract_login_candidates(details)
contact_id = _match_contact(full_name, company) if full_name and company else None
if not contact_id:
for login_candidate in login_candidates:
contact_id = _match_contact_by_login(login_candidate, company)
if contact_id:
break
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()