From d5578f56ecfe7d86a97f4d079fcff8855e6874db Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Sun, 5 Oct 2025 03:36:56 -0600 Subject: [PATCH] Fix agent hash discovery and add lookup endpoint --- Data/Agent/Roles/role_DeviceAudit.py | 46 +++++++++++++---- Data/Server/server.py | 77 ++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 10 deletions(-) diff --git a/Data/Agent/Roles/role_DeviceAudit.py b/Data/Agent/Roles/role_DeviceAudit.py index 0162fb6..bf0e735 100644 --- a/Data/Agent/Roles/role_DeviceAudit.py +++ b/Data/Agent/Roles/role_DeviceAudit.py @@ -145,17 +145,43 @@ _AGENT_HASH_CACHE = { def _iter_hash_roots(): - seen = set() + """Yield candidate folders that may contain github_repo_hash.txt.""" + root = _project_root() - for _ in range(6): - if not root or root in seen: - break - yield root - seen.add(root) - parent = os.path.dirname(root) - if not parent or parent == root: - break - root = parent + if not root: + return + + # Breadth-first walk up to a small, bounded set of parents/siblings. + seen = set() + queue = [root] + + # Some deployments place the hash file directly under Agent/, while others + # (including the scheduled updater) write to Agent/Borealis/. The previous + # implementation only checked the parent chain which skipped Agent/Borealis, + # so seed the queue with that sibling when available. + borealis = os.path.join(root, "Borealis") + if os.path.isdir(borealis): + queue.append(borealis) + + steps = 0 + while queue and steps < 12: # hard stop to avoid wandering too far + steps += 1 + cur = queue.pop(0) + if not cur or cur in seen: + continue + seen.add(cur) + yield cur + + parent = os.path.dirname(cur) + if parent and parent != cur and parent not in seen: + queue.append(parent) + + # If we're currently at Agent/ or its parent, also check for an adjacent + # Borealis/ folder in case the hash lives there. + if cur != borealis: + candidate = os.path.join(cur, "Borealis") + if os.path.isdir(candidate) and candidate not in seen: + queue.append(candidate) def _resolve_git_head_hash(root: str) -> Optional[str]: diff --git a/Data/Server/server.py b/Data/Server/server.py index 515482e..eb55409 100644 --- a/Data/Server/server.py +++ b/Data/Server/server.py @@ -3138,6 +3138,83 @@ def set_device_description(hostname: str): return jsonify({"error": str(e)}), 500 +@app.route("/api/agent/hash/", methods=["GET"]) +def get_agent_hash(agent_id: str): + """Return the last known github_repo_hash for a specific agent.""" + + agent_id = (agent_id or "").strip() + if not agent_id: + return jsonify({"error": "invalid agent id"}), 400 + + # Prefer the in-memory registry (updated on every heartbeat/details post). + info = registered_agents.get(agent_id) or {} + candidate = (info.get("agent_hash") or "").strip() + hostname = (info.get("hostname") or "").strip() + + if candidate: + return jsonify({ + "agent_id": agent_id, + "agent_hash": candidate, + "source": "memory", + }) + + # Fall back to the persisted device_details row, if any. + try: + conn = _db_conn() + cur = conn.cursor() + + row = None + if hostname: + cur.execute( + "SELECT agent_hash, details FROM device_details WHERE hostname = ?", + (hostname,), + ) + row = cur.fetchone() + else: + # No hostname available; scan for a matching agent_id in the JSON payload. + cur.execute("SELECT hostname, agent_hash, details FROM device_details") + for host, db_hash, details_json in cur.fetchall(): + try: + data = json.loads(details_json or "{}") + except Exception: + data = {} + summary = data.get("summary") or {} + if (summary.get("agent_id") or "").strip() == agent_id: + row = (db_hash, details_json) + hostname = host or hostname + break + + conn.close() + + if row: + db_hash = (row[0] or "").strip() + if db_hash: + return jsonify({ + "agent_id": agent_id, + "agent_hash": db_hash, + "hostname": hostname, + "source": "database", + }) + # Hash column may be empty if only stored inside details JSON. + try: + details = json.loads(row[1] or "{}") + except Exception: + details = {} + summary = details.get("summary") or {} + summary_hash = (summary.get("agent_hash") or "").strip() + if summary_hash: + return jsonify({ + "agent_id": agent_id, + "agent_hash": summary_hash, + "hostname": hostname, + "source": "database", + }) + + return jsonify({"error": "agent hash not found"}), 404 + except Exception as e: + return jsonify({"error": str(e)}), 500 + + # --------------------------------------------- # Quick Job Execution + Activity History # ---------------------------------------------