diff --git a/Data/Agent/agent.py b/Data/Agent/agent.py index 1cca901..9140ad9 100644 --- a/Data/Agent/agent.py +++ b/Data/Agent/agent.py @@ -535,6 +535,38 @@ def _settings_dir(): except Exception: return os.path.abspath(os.path.join(os.path.dirname(__file__), 'Settings')) + +def _agent_guid_path() -> str: + try: + root = _find_project_root() + return os.path.join(root, 'Agent', 'Borealis', 'agent_GUID') + except Exception: + return os.path.abspath(os.path.join(os.path.dirname(__file__), 'agent_GUID')) + + +def _persist_agent_guid_local(guid: str): + guid = (guid or '').strip() + if not guid: + return + path = _agent_guid_path() + try: + directory = os.path.dirname(path) + if directory: + os.makedirs(directory, exist_ok=True) + existing = '' + if os.path.isfile(path): + try: + with open(path, 'r', encoding='utf-8') as fh: + existing = fh.read().strip() + except Exception: + existing = '' + if existing != guid: + with open(path, 'w', encoding='utf-8') as fh: + fh.write(guid) + except Exception as exc: + _log_agent(f'Failed to persist agent GUID locally: {exc}', fname='agent.error.log') + + def get_server_url() -> str: """Return the Borealis server URL from env or Agent/Borealis/Settings/server_url.txt. - Strips UTF-8 BOM and whitespace @@ -1214,7 +1246,16 @@ async def connect(): payload = {"agent_id": AGENT_ID, "hostname": socket.gethostname(), "username": ".\\svcBorealis"} timeout = aiohttp.ClientTimeout(total=10) async with aiohttp.ClientSession(timeout=timeout) as session: - await session.post(url, json=payload) + async with session.post(url, json=payload) as resp: + if resp.status == 200: + try: + data = await resp.json(content_type=None) + except Exception: + data = None + if isinstance(data, dict): + guid_value = (data.get('agent_guid') or '').strip() + if guid_value: + _persist_agent_guid_local(guid_value) except Exception: pass asyncio.create_task(_svc_checkin_once()) diff --git a/Data/Server/server.py b/Data/Server/server.py index d185b3b..d769123 100644 --- a/Data/Server/server.py +++ b/Data/Server/server.py @@ -401,6 +401,7 @@ def _lookup_agent_hash_record(agent_id: str) -> Optional[Dict[str, Any]]: info = registered_agents.get(agent_id) or {} candidate = (info.get('agent_hash') or '').strip() + candidate_guid = _normalize_guid(info.get('agent_guid')) if info.get('agent_guid') else '' hostname = (info.get('hostname') or '').strip() if candidate: payload: Dict[str, Any] = { @@ -410,6 +411,8 @@ def _lookup_agent_hash_record(agent_id: str) -> Optional[Dict[str, Any]]: } if hostname: payload['hostname'] = hostname + if candidate_guid: + payload['agent_guid'] = candidate_guid return payload conn = None @@ -437,6 +440,9 @@ def _lookup_agent_hash_record(agent_id: str) -> Optional[Dict[str, Any]]: 'hostname': row.get('hostname') or effective_hostname, 'source': 'database', } + row_guid = _normalize_guid(row.get('guid')) if row.get('guid') else '' + if row_guid: + payload['agent_guid'] = row_guid return payload first = rows[0] fallback_hash = (first.get('agent_hash') or '').strip() @@ -453,9 +459,12 @@ def _lookup_agent_hash_record(agent_id: str) -> Optional[Dict[str, Any]]: 'hostname': first.get('hostname') or effective_hostname, 'source': 'database', } + row_guid = _normalize_guid(first.get('guid')) if first.get('guid') else '' + if row_guid: + payload['agent_guid'] = row_guid return payload - cur.execute('SELECT hostname, agent_hash, details FROM device_details') - for host, db_hash, details_json in cur.fetchall(): + cur.execute('SELECT hostname, agent_hash, details, guid FROM device_details') + for host, db_hash, details_json, row_guid in cur.fetchall(): try: data = json.loads(details_json or '{}') except Exception: @@ -467,12 +476,18 @@ def _lookup_agent_hash_record(agent_id: str) -> Optional[Dict[str, Any]]: summary_hash = (summary.get('agent_hash') or '').strip() normalized_hash = (db_hash or '').strip() or summary_hash if normalized_hash: - return { + payload = { 'agent_id': agent_id, 'agent_hash': normalized_hash, 'hostname': host, 'source': 'database', } + normalized_guid = _normalize_guid(row_guid) if row_guid else '' + if not normalized_guid: + normalized_guid = _normalize_guid(summary.get('agent_guid')) if summary.get('agent_guid') else '' + if normalized_guid: + payload['agent_guid'] = normalized_guid + return payload finally: if conn: try: @@ -482,41 +497,156 @@ def _lookup_agent_hash_record(agent_id: str) -> Optional[Dict[str, Any]]: return None -def _apply_agent_hash_update(agent_id: str, agent_hash: str) -> Tuple[Dict[str, Any], int]: +def _lookup_agent_hash_by_guid(agent_guid: str) -> Optional[Dict[str, Any]]: + normalized_guid = _normalize_guid(agent_guid) + if not normalized_guid: + return None + + # Prefer in-memory record when available + for aid, rec in registered_agents.items(): + try: + if _normalize_guid(rec.get('agent_guid')) == normalized_guid: + candidate_hash = (rec.get('agent_hash') or '').strip() + if candidate_hash: + payload: Dict[str, Any] = { + 'agent_guid': normalized_guid, + 'agent_hash': candidate_hash, + 'source': 'memory', + } + hostname = (rec.get('hostname') or '').strip() + if hostname: + payload['hostname'] = hostname + if aid: + payload['agent_id'] = aid + return payload + break + except Exception: + continue + + conn = None + try: + conn = _db_conn() + cur = conn.cursor() + cur.execute( + 'SELECT hostname, agent_hash, details FROM device_details WHERE guid = ?', + (normalized_guid,), + ) + row = cur.fetchone() + if not row: + return None + hostname, agent_hash, details_json = row + try: + details = json.loads(details_json or '{}') + except Exception: + details = {} + summary = details.get('summary') or {} + agent_id = (summary.get('agent_id') or '').strip() + normalized_hash = (agent_hash or summary.get('agent_hash') or '').strip() + payload = { + 'agent_guid': normalized_guid, + 'agent_hash': normalized_hash, + 'hostname': hostname, + 'source': 'database', + } + if agent_id: + payload['agent_id'] = agent_id + return payload + finally: + if conn: + try: + conn.close() + except Exception: + pass + + +def _apply_agent_hash_update(agent_id: str, agent_hash: str, agent_guid: Optional[str] = None) -> Tuple[Dict[str, Any], int]: agent_id = (agent_id or '').strip() agent_hash = (agent_hash or '').strip() - if not agent_id or not agent_hash: - return {'error': 'agent_id and agent_hash required'}, 400 + normalized_guid = _normalize_guid(agent_guid) + if not agent_hash or (not agent_id and not normalized_guid): + return {'error': 'agent_hash and agent_guid or agent_id required'}, 400 conn = None hostname = None + resolved_agent_id = agent_id response_payload: Optional[Dict[str, Any]] = None status_code = 200 try: conn = _db_conn() cur = conn.cursor() - rows = _device_rows_for_agent(cur, agent_id) - target = None - for row in rows: - if row.get('matched'): - target = row - break - if not target: + updated_via_guid = False + if normalized_guid: + cur.execute( + 'SELECT hostname, details FROM device_details WHERE guid = ?', + (normalized_guid,), + ) + row = cur.fetchone() + if row: + updated_via_guid = True + hostname = row[0] + try: + details = json.loads(row[1] or '{}') + except Exception: + details = {} + summary = details.setdefault('summary', {}) + if not resolved_agent_id: + resolved_agent_id = (summary.get('agent_id') or '').strip() + if resolved_agent_id: + summary['agent_id'] = resolved_agent_id + summary['agent_hash'] = agent_hash + summary['agent_guid'] = normalized_guid + cur.execute( + 'UPDATE device_details SET agent_hash=?, details=? WHERE hostname=?', + (agent_hash, json.dumps(details), hostname), + ) + conn.commit() + elif not agent_id: + response_payload = { + 'status': 'ignored', + 'agent_guid': normalized_guid, + 'agent_hash': agent_hash, + } + + if response_payload is None and not updated_via_guid: + target = None + rows = _device_rows_for_agent(cur, resolved_agent_id) + for row in rows: + if row.get('matched'): + target = row + break + if not target and rows: + target = rows[0] + if not target: + response_payload = { + 'status': 'ignored', + 'agent_id': resolved_agent_id, + 'agent_hash': agent_hash, + } + else: + hostname = target.get('hostname') + details = target.get('details') or {} + summary = details.setdefault('summary', {}) + summary['agent_hash'] = agent_hash + if normalized_guid: + summary['agent_guid'] = normalized_guid + if resolved_agent_id: + summary['agent_id'] = resolved_agent_id + cur.execute( + 'UPDATE device_details SET agent_hash=?, details=? WHERE hostname=?', + (agent_hash, json.dumps(details), hostname), + ) + conn.commit() + if not normalized_guid: + normalized_guid = _normalize_guid(target.get('guid')) if target.get('guid') else '' + if not normalized_guid: + normalized_guid = _normalize_guid((summary.get('agent_guid') or '')) if summary else '' + elif response_payload is None and normalized_guid and not hostname: + # GUID provided and update attempted but hostname not resolved response_payload = { 'status': 'ignored', - 'agent_id': agent_id, + 'agent_guid': normalized_guid, 'agent_hash': agent_hash, } - else: - hostname = target.get('hostname') - details = target.get('details') or {} - summary = details.setdefault('summary', {}) - summary['agent_hash'] = agent_hash - cur.execute( - 'UPDATE device_details SET agent_hash=?, details=? WHERE hostname=?', - (agent_hash, json.dumps(details), hostname), - ) - conn.commit() except Exception as exc: if conn: try: @@ -536,36 +666,59 @@ def _apply_agent_hash_update(agent_id: str, agent_hash: str) -> Tuple[Dict[str, return response_payload, status_code normalized_hash = agent_hash - if agent_id in registered_agents: - registered_agents[agent_id]['agent_hash'] = normalized_hash - try: - for aid, rec in registered_agents.items(): - if rec.get('hostname') and hostname and rec['hostname'] == hostname: - rec['agent_hash'] = normalized_hash - except Exception: - pass + if normalized_guid: + try: + for aid, rec in registered_agents.items(): + guid_candidate = _normalize_guid(rec.get('agent_guid')) + if guid_candidate == normalized_guid or (resolved_agent_id and aid == resolved_agent_id): + rec['agent_hash'] = normalized_hash + rec['agent_guid'] = normalized_guid + if not resolved_agent_id: + resolved_agent_id = aid + except Exception: + pass + if resolved_agent_id and resolved_agent_id in registered_agents: + registered_agents[resolved_agent_id]['agent_hash'] = normalized_hash + if normalized_guid: + registered_agents[resolved_agent_id]['agent_guid'] = normalized_guid + if hostname: + try: + for aid, rec in registered_agents.items(): + if rec.get('hostname') and rec['hostname'] == hostname: + rec['agent_hash'] = normalized_hash + if normalized_guid: + rec['agent_guid'] = normalized_guid + except Exception: + pass payload: Dict[str, Any] = { 'status': 'ok', - 'agent_id': agent_id, 'agent_hash': agent_hash, } + if resolved_agent_id: + payload['agent_id'] = resolved_agent_id if hostname: payload['hostname'] = hostname + if normalized_guid: + payload['agent_guid'] = normalized_guid return payload, 200 @app.route("/api/agent/hash", methods=["GET", "POST"]) def api_agent_hash(): if request.method == 'GET': + agent_guid = _normalize_guid(request.args.get('agent_guid')) agent_id = (request.args.get('agent_id') or request.args.get('id') or '').strip() - if not agent_id: + if not agent_guid and not agent_id: data = request.get_json(silent=True) or {} - agent_id = (data.get('agent_id') or '').strip() - if not agent_id: - return jsonify({'error': 'agent_id required'}), 400 + agent_guid = _normalize_guid(data.get('agent_guid')) if data else agent_guid + agent_id = (data.get('agent_id') or '').strip() if data else agent_id try: - record = _lookup_agent_hash_record(agent_id) + record = None + if agent_guid: + record = _lookup_agent_hash_by_guid(agent_guid) + if not record and agent_id: + record = _lookup_agent_hash_record(agent_id) except Exception as exc: _write_service_log('server', f'/api/agent/hash lookup error: {exc}') return jsonify({'error': 'internal error'}), 500 @@ -576,7 +729,8 @@ def api_agent_hash(): data = request.get_json(silent=True) or {} agent_id = (data.get('agent_id') or '').strip() agent_hash = (data.get('agent_hash') or '').strip() - payload, status = _apply_agent_hash_update(agent_id, agent_hash) + agent_guid = _normalize_guid(data.get('agent_guid')) if data else None + payload, status = _apply_agent_hash_update(agent_id, agent_hash, agent_guid) return jsonify(payload), status @@ -747,6 +901,7 @@ def api_login(): now = _now_ts() cur.execute("UPDATE users SET last_login=?, updated_at=? WHERE id=?", (now, now, row[0])) conn.commit() + conn.commit() conn.close() # set session cookie session['username'] = row[1] @@ -2209,7 +2364,7 @@ def init_db(): # Device details table cur.execute( - "CREATE TABLE IF NOT EXISTS device_details (hostname TEXT PRIMARY KEY, description TEXT, details TEXT, created_at INTEGER, agent_hash TEXT)" + "CREATE TABLE IF NOT EXISTS device_details (hostname TEXT PRIMARY KEY, description TEXT, details TEXT, created_at INTEGER, agent_hash TEXT, guid TEXT)" ) # Backfill missing created_at column on existing installs try: @@ -2219,6 +2374,8 @@ def init_db(): cur.execute("ALTER TABLE device_details ADD COLUMN created_at INTEGER") if 'agent_hash' not in cols: cur.execute("ALTER TABLE device_details ADD COLUMN agent_hash TEXT") + if 'guid' not in cols: + cur.execute("ALTER TABLE device_details ADD COLUMN guid TEXT") except Exception: pass @@ -2980,7 +3137,7 @@ def _persist_last_seen(hostname: str, last_seen: int, agent_id: str = None): conn = _db_conn() cur = conn.cursor() cur.execute( - "SELECT details, description, created_at FROM device_details WHERE hostname = ?", + "SELECT details, description, created_at, guid FROM device_details WHERE hostname = ?", (hostname,), ) row = cur.fetchone() @@ -3007,6 +3164,12 @@ def _persist_last_seen(hostname: str, last_seen: int, agent_id: str = None): summary["agent_id"] = str(agent_id) except Exception: pass + try: + existing_guid = (row[3] or "").strip() if row and len(row) > 3 else "" + except Exception: + existing_guid = "" + if existing_guid and not summary.get("agent_guid"): + summary["agent_guid"] = _normalize_guid(existing_guid) details["summary"] = summary now = int(time.time()) @@ -3042,8 +3205,9 @@ def load_agents_from_db(): try: conn = _db_conn() cur = conn.cursor() - cur.execute("SELECT hostname, details, agent_hash FROM device_details") - for hostname, details_json, agent_hash in cur.fetchall(): + cur.execute("SELECT hostname, details, agent_hash, guid FROM device_details") + rows = cur.fetchall() + for hostname, details_json, agent_hash, guid in rows: try: details = json.loads(details_json or "{}") except Exception: @@ -3055,6 +3219,16 @@ def load_agents_from_db(): stored_hash = (agent_hash or summary.get("agent_hash") or "").strip() except Exception: stored_hash = summary.get("agent_hash") or "" + agent_guid = (summary.get("agent_guid") or guid or "").strip() + if guid and not summary.get("agent_guid"): + summary["agent_guid"] = _normalize_guid(guid) + try: + cur.execute( + "UPDATE device_details SET details=? WHERE hostname=?", + (json.dumps(details), hostname), + ) + except Exception: + pass registered_agents[agent_id] = { "agent_id": agent_id, "hostname": summary.get("hostname") or hostname, @@ -3067,6 +3241,8 @@ def load_agents_from_db(): } if stored_hash: registered_agents[agent_id]["agent_hash"] = stored_hash + if agent_guid: + registered_agents[agent_id]["agent_guid"] = _normalize_guid(agent_guid) conn.close() except Exception as e: print(f"[WARN] Failed to load agents from DB: {e}") @@ -3100,13 +3276,13 @@ def _device_rows_for_agent(cur, agent_id: str) -> List[Dict[str, Any]]: return results try: cur.execute( - "SELECT hostname, agent_hash, details FROM device_details WHERE LOWER(hostname) = ?", + "SELECT hostname, agent_hash, details, guid FROM device_details WHERE LOWER(hostname) = ?", (base_host.lower(),), ) rows = cur.fetchall() except Exception: return results - for hostname, agent_hash, details_json in rows or []: + for hostname, agent_hash, details_json, guid in rows or []: try: details = json.loads(details_json or "{}") except Exception: @@ -3121,10 +3297,121 @@ def _device_rows_for_agent(cur, agent_id: str) -> List[Dict[str, Any]]: "details": details, "summary_agent_id": summary_agent, "matched": matched, + "guid": (guid or "").strip(), } ) return results + +def _normalize_guid(value: Optional[str]) -> str: + candidate = (value or "").strip() + if not candidate: + return "" + try: + return str(uuid.UUID(candidate)) + except Exception: + return candidate + + +def _ensure_agent_guid_for_hostname(cur, hostname: str, agent_id: Optional[str] = None) -> Optional[str]: + normalized_host = (hostname or "").strip() + if not normalized_host: + return None + cur.execute( + "SELECT hostname, guid, details FROM device_details WHERE LOWER(hostname) = ?", + (normalized_host.lower(),), + ) + row = cur.fetchone() + if row: + actual_host, existing_guid, details_json = row + else: + actual_host, existing_guid, details_json = normalized_host, "", "{}" + try: + details = json.loads(details_json or "{}") + except Exception: + details = {} + summary = details.setdefault("summary", {}) + if agent_id and not summary.get("agent_id"): + try: + summary["agent_id"] = str(agent_id) + except Exception: + summary["agent_id"] = agent_id + if actual_host and not summary.get("hostname"): + summary["hostname"] = actual_host + + if existing_guid: + normalized = _normalize_guid(existing_guid) + summary.setdefault("agent_guid", normalized) + cur.execute( + "UPDATE device_details SET guid=?, details=? WHERE hostname=?", + (normalized, json.dumps(details), actual_host), + ) + return summary.get("agent_guid") or normalized + + new_guid = str(uuid.uuid4()) + summary["agent_guid"] = new_guid + now = int(time.time()) + cur.execute( + """ + INSERT INTO device_details(hostname, description, details, created_at, guid) + VALUES (?, COALESCE((SELECT description FROM device_details WHERE hostname = ?), ''), ?, COALESCE((SELECT created_at FROM device_details WHERE hostname = ?), ?), ?) + ON CONFLICT(hostname) DO UPDATE SET + guid=excluded.guid, + details=excluded.details, + created_at=COALESCE(device_details.created_at, excluded.created_at), + description=COALESCE(device_details.description, excluded.description) + """, + (actual_host, actual_host, json.dumps(details), actual_host, now, new_guid), + ) + return new_guid + + +def _ensure_agent_guid(agent_id: str, hostname: Optional[str] = None) -> Optional[str]: + agent_id = (agent_id or "").strip() + normalized_host = (hostname or "").strip() + conn = None + try: + conn = _db_conn() + cur = conn.cursor() + rows = _device_rows_for_agent(cur, agent_id) if agent_id else [] + for row in rows: + candidate = _normalize_guid(row.get("guid")) + if candidate: + summary = row.get("details", {}).setdefault("summary", {}) + summary.setdefault("agent_guid", candidate) + cur.execute( + "UPDATE device_details SET guid=?, details=? WHERE hostname=?", + (candidate, json.dumps(row.get("details", {})), row.get("hostname")), + ) + conn.commit() + return candidate + + target_host = normalized_host + if not target_host: + for row in rows: + if row.get("matched"): + target_host = row.get("hostname") + break + if not target_host and rows: + target_host = rows[0].get("hostname") + if not target_host and agent_id and agent_id in registered_agents: + target_host = registered_agents[agent_id].get("hostname") + if not target_host: + return None + + guid = _ensure_agent_guid_for_hostname(cur, target_host, agent_id) + conn.commit() + return guid + except Exception as exc: + _write_service_log('server', f'ensure_agent_guid failure for {agent_id or hostname}: {exc}') + return None + finally: + if conn: + try: + conn.close() + except Exception: + pass + @app.route("/api/agents") def get_agents(): """Return agents with collector activity indicator.""" @@ -3188,6 +3475,11 @@ def save_agent_details(): agent_hash = agent_hash.strip() or None else: agent_hash = None + agent_guid = data.get("agent_guid") + if isinstance(agent_guid, str): + agent_guid = agent_guid.strip() or None + else: + agent_guid = None if not hostname and isinstance(details, dict): hostname = (details.get("summary") or {}).get("hostname") if not hostname or not isinstance(details, dict): @@ -3197,13 +3489,14 @@ def save_agent_details(): cur = conn.cursor() # Load existing row to preserve description and created_at and merge fields cur.execute( - "SELECT details, description, created_at FROM device_details WHERE hostname = ?", + "SELECT details, description, created_at, guid FROM device_details WHERE hostname = ?", (hostname,), ) row = cur.fetchone() prev_details = {} description = "" created_at = 0 + existing_guid = None if row: try: prev_details = json.loads(row[0] or '{}') @@ -3214,6 +3507,12 @@ def save_agent_details(): created_at = int(row[2] or 0) except Exception: created_at = 0 + try: + existing_guid = (row[3] or "").strip() + except Exception: + existing_guid = None + else: + existing_guid = None # Ensure summary exists and attach hostname/agent_id if missing incoming_summary = details.setdefault("summary", {}) @@ -3229,6 +3528,10 @@ def save_agent_details(): incoming_summary["agent_hash"] = agent_hash except Exception: pass + effective_guid = agent_guid or existing_guid + normalized_effective_guid = _normalize_guid(effective_guid) if effective_guid else None + if normalized_effective_guid: + incoming_summary["agent_guid"] = normalized_effective_guid # Preserve last_seen if incoming omitted it if not incoming_summary.get("last_seen"): @@ -3284,15 +3587,23 @@ def save_agent_details(): # Upsert row without destroying created_at; keep previous created_at if exists cur.execute( """ - INSERT INTO device_details(hostname, description, details, created_at, agent_hash) - VALUES (?,?,?,?,?) + INSERT INTO device_details(hostname, description, details, created_at, agent_hash, guid) + VALUES (?,?,?,?,?,?) ON CONFLICT(hostname) DO UPDATE SET description=excluded.description, details=excluded.details, created_at=COALESCE(device_details.created_at, excluded.created_at), - agent_hash=COALESCE(NULLIF(excluded.agent_hash, ''), device_details.agent_hash) + agent_hash=COALESCE(NULLIF(excluded.agent_hash, ''), device_details.agent_hash), + guid=COALESCE(NULLIF(excluded.guid, ''), device_details.guid) """, - (hostname, description, json.dumps(merged), created_at, agent_hash or None), + ( + hostname, + description, + json.dumps(merged), + created_at, + agent_hash or None, + (normalized_effective_guid or None), + ), ) conn.commit() conn.close() @@ -3302,16 +3613,20 @@ def save_agent_details(): normalized_hash = (agent_hash or (merged.get("summary") or {}).get("agent_hash") or "").strip() except Exception: normalized_hash = agent_hash - if normalized_hash: - if agent_id and agent_id in registered_agents: + if agent_id and agent_id in registered_agents: + if normalized_hash: registered_agents[agent_id]["agent_hash"] = normalized_hash - # Also update any entries keyed by hostname (duplicate agents) - try: - for aid, rec in registered_agents.items(): - if rec.get("hostname") == hostname and normalized_hash: - rec["agent_hash"] = normalized_hash - except Exception: - pass + if normalized_effective_guid: + registered_agents[agent_id]["agent_guid"] = normalized_effective_guid + # Also update any entries keyed by hostname (duplicate agents) + try: + for aid, rec in registered_agents.items(): + if rec.get("hostname") == hostname and normalized_hash: + rec["agent_hash"] = normalized_hash + if rec.get("hostname") == hostname and normalized_effective_guid: + rec["agent_guid"] = normalized_effective_guid + except Exception: + pass return jsonify({"status": "ok"}) except Exception as e: return jsonify({"error": str(e)}), 500 @@ -3323,7 +3638,7 @@ def get_device_details(hostname: str): conn = _db_conn() cur = conn.cursor() cur.execute( - "SELECT details, description, created_at, agent_hash FROM device_details WHERE hostname = ?", + "SELECT details, description, created_at, agent_hash, guid FROM device_details WHERE hostname = ?", (hostname,), ) row = cur.fetchone() @@ -3352,6 +3667,15 @@ def get_device_details(hostname: str): details['summary']['agent_hash'] = agent_hash except Exception: pass + if len(row) > 4: + agent_guid = (row[4] or "").strip() + if agent_guid: + try: + details.setdefault('summary', {}) + if not details['summary'].get('agent_guid'): + details['summary']['agent_guid'] = _normalize_guid(agent_guid) + except Exception: + pass return jsonify(details) except Exception: pass @@ -3915,6 +4239,7 @@ def api_agent_checkin(): username = raw_username or DEFAULT_SERVICE_ACCOUNT if username in LEGACY_SERVICE_ACCOUNTS: username = DEFAULT_SERVICE_ACCOUNT + hostname = (payload.get('hostname') or '').strip() try: conn = _db_conn() row = _service_acct_get(conn, agent_id) @@ -3947,10 +4272,22 @@ def api_agent_checkin(): } conn.close() _ansible_log_server(f"[checkin] return creds agent_id={agent_id} user={out['username']}") + try: + if hostname: + _persist_last_seen(hostname, int(time.time()), agent_id) + except Exception: + pass + agent_guid = _ensure_agent_guid(agent_id, hostname or None) + if agent_guid and agent_id: + rec = registered_agents.setdefault(agent_id, {}) + rec['agent_guid'] = agent_guid + else: + agent_guid = agent_guid or '' return jsonify({ 'username': out['username'], 'password': out['password'], - 'policy': { 'force_rotation_minutes': 43200 } + 'policy': { 'force_rotation_minutes': 43200 }, + 'agent_guid': agent_guid or None, }) except Exception as e: _ansible_log_server(f"[checkin] error agent_id={agent_id} err={e}") diff --git a/Update.ps1 b/Update.ps1 index e4109a8..326f569 100644 --- a/Update.ps1 +++ b/Update.ps1 @@ -119,6 +119,7 @@ function Get-AgentServiceId { $settingsDir = Join-Path $AgentRoot 'Settings' $candidates = @( (Join-Path $settingsDir 'agent_settings_svc.json') + (Join-Path $settingsDir 'agent_settings_user.json') (Join-Path $settingsDir 'agent_settings.json') ) @@ -137,6 +138,29 @@ function Get-AgentServiceId { return '' } +function Get-AgentGuid { + param( + [string]$AgentRoot + ) + + $candidates = @() + if (-not $AgentRoot) { $AgentRoot = $scriptDir } + if ($AgentRoot) { $candidates += (Join-Path $AgentRoot 'agent_GUID') } + $defaultPath = Join-Path $scriptDir 'Agent\Borealis\agent_GUID' + if ($defaultPath -and ($candidates -notcontains $defaultPath)) { $candidates += $defaultPath } + + foreach ($path in ($candidates | Select-Object -Unique)) { + try { + if (Test-Path $path -PathType Leaf) { + $value = (Get-Content -Path $path -Raw -ErrorAction Stop) + if ($value) { return $value.Trim() } + } + } catch {} + } + + return '' +} + function Get-RepositoryCommitHash { param( [Parameter(Mandatory = $true)] @@ -310,16 +334,21 @@ function Submit-AgentHash { [string]$AgentId, [Parameter(Mandatory = $true)] - [string]$AgentHash + [string]$AgentHash, + + [string]$AgentGuid ) - if ([string]::IsNullOrWhiteSpace($ServerBaseUrl) -or [string]::IsNullOrWhiteSpace($AgentId) -or [string]::IsNullOrWhiteSpace($AgentHash)) { + if ([string]::IsNullOrWhiteSpace($ServerBaseUrl) -or [string]::IsNullOrWhiteSpace($AgentHash)) { return } $base = $ServerBaseUrl.TrimEnd('/') $uri = "$base/api/agent/hash" - $payload = @{ agent_id = $AgentId; agent_hash = $AgentHash } | ConvertTo-Json -Depth 3 + $payloadBody = @{ agent_hash = $AgentHash } + if (-not [string]::IsNullOrWhiteSpace($AgentId)) { $payloadBody.agent_id = $AgentId } + if (-not [string]::IsNullOrWhiteSpace($AgentGuid)) { $payloadBody.agent_guid = $AgentGuid } + $payload = $payloadBody | ConvertTo-Json -Depth 3 $headers = @{ 'User-Agent' = 'borealis-agent-updater' } $resp = Invoke-WebRequest -Uri $uri -Method Post -Headers $headers -Body $payload -ContentType 'application/json' -UseBasicParsing -ErrorAction Stop @@ -338,6 +367,7 @@ function Sync-AgentHashRecord { [string]$AgentHash, [string]$ServerBaseUrl, [string]$AgentId, + [string]$AgentGuid, [string]$BranchName = 'main' ) @@ -354,13 +384,13 @@ function Sync-AgentHashRecord { Write-Host ("Submitting agent hash to server: {0}" -f $AgentHash) - if ([string]::IsNullOrWhiteSpace($AgentId)) { - Write-Host "Agent ID unavailable; skipping agent hash submission." -ForegroundColor DarkYellow + if ([string]::IsNullOrWhiteSpace($AgentId) -and [string]::IsNullOrWhiteSpace($AgentGuid)) { + Write-Host "Agent identifier unavailable; skipping agent hash submission." -ForegroundColor DarkYellow return } try { - $submitResult = Submit-AgentHash -ServerBaseUrl $ServerBaseUrl -AgentId $AgentId -AgentHash $AgentHash + $submitResult = Submit-AgentHash -ServerBaseUrl $ServerBaseUrl -AgentId $AgentId -AgentHash $AgentHash -AgentGuid $AgentGuid if ($submitResult -and ($submitResult.status -eq 'ok')) { Write-Host "Server agent_hash database record updated successfully." } elseif ($submitResult -and ($submitResult.status -eq 'ignored')) { @@ -441,6 +471,15 @@ function Invoke-BorealisAgentUpdate { } } + $agentGuid = Get-AgentGuid -AgentRoot $agentRoot + if ($agentGuid) { + Write-Host ("Agent GUID: {0}" -f $agentGuid) + } else { + Write-Host "Warning: No agent GUID detected - Please deploy the agent, associating it with a Borealis server then try running the updater script again." -ForegroundColor Yellow + Write-Host "⚠️ Borealis update aborted." + return + } + $currentHash = Get-RepositoryCommitHash -ProjectRoot $scriptDir -AgentRoot $agentRoot $serverBaseUrl = Get-BorealisServerUrl -AgentRoot $agentRoot $agentId = Get-AgentServiceId -AgentRoot $agentRoot @@ -485,7 +524,7 @@ function Invoke-BorealisAgentUpdate { return } elseif (-not $needsUpdate) { Write-Host "Local agent files already match the server repository hash." -ForegroundColor Green - Sync-AgentHashRecord -ProjectRoot $scriptDir -AgentRoot $agentRoot -AgentHash $serverHash -ServerBaseUrl $serverBaseUrl -AgentId $agentId -BranchName $serverBranch + Sync-AgentHashRecord -ProjectRoot $scriptDir -AgentRoot $agentRoot -AgentHash $serverHash -ServerBaseUrl $serverBaseUrl -AgentId $agentId -AgentGuid $agentGuid -BranchName $serverBranch Write-Host "✅ Borealis - Automation Platform Already Up-to-Date" return } else { @@ -555,7 +594,7 @@ function Invoke-BorealisAgentUpdate { } if ($newHash) { - Sync-AgentHashRecord -ProjectRoot $scriptDir -AgentRoot $agentRoot -AgentHash $newHash -ServerBaseUrl $serverBaseUrl -AgentId $agentId -BranchName $serverBranch + Sync-AgentHashRecord -ProjectRoot $scriptDir -AgentRoot $agentRoot -AgentHash $newHash -ServerBaseUrl $serverBaseUrl -AgentId $agentId -AgentGuid $agentGuid -BranchName $serverBranch } else { Write-Host "Unable to determine repository hash for submission; server hash not updated." -ForegroundColor DarkYellow }