- 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.
309 lines
10 KiB
Python
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()
|