From 91aafc305d794ac42c13d78dbf3e5017cdef2197 Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Sat, 27 Sep 2025 21:53:23 -0600 Subject: [PATCH] Fixed Last User Race Condition --- Data/Agent/agent.py | 51 +++++++++++++++++++++++++++++-------------- Data/Server/server.py | 49 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 82 insertions(+), 18 deletions(-) diff --git a/Data/Agent/agent.py b/Data/Agent/agent.py index adc5032..f0655b6 100644 --- a/Data/Agent/agent.py +++ b/Data/Agent/agent.py @@ -660,14 +660,26 @@ async def send_heartbeat(): "last_seen": int(time.time()) } await sio.emit("agent_heartbeat", payload) - # Also report collector status alive ping with last_user - import getpass - await sio.emit('collector_status', { - 'agent_id': AGENT_ID, - 'hostname': socket.gethostname(), - 'active': True, - 'last_user': f"{os.environ.get('USERDOMAIN') or socket.gethostname()}\\{getpass.getuser()}" - }) + # Also report collector status alive ping. + # To avoid clobbering last_user with SYSTEM/machine accounts, + # only include last_user from the interactive agent. + try: + if not SYSTEM_SERVICE_MODE: + import getpass + await sio.emit('collector_status', { + 'agent_id': AGENT_ID, + 'hostname': socket.gethostname(), + 'active': True, + 'last_user': f"{os.environ.get('USERDOMAIN') or socket.gethostname()}\\{getpass.getuser()}" + }) + else: + await sio.emit('collector_status', { + 'agent_id': AGENT_ID, + 'hostname': socket.gethostname(), + 'active': True, + }) + except Exception: + pass except Exception as e: print(f"[WARN] heartbeat emit failed: {e}") # Send periodic heartbeats every 60 seconds @@ -1003,15 +1015,22 @@ async def connect(): print(f"[WARN] initial heartbeat failed: {e}") _log_agent(f'Initial heartbeat failed: {e}', fname='agent.error.log') - # Let server know collector is active and who the user is + # Let server know collector is active; send last_user only from interactive agent try: - import getpass - await sio.emit('collector_status', { - 'agent_id': AGENT_ID, - 'hostname': socket.gethostname(), - 'active': True, - 'last_user': f"{os.environ.get('USERDOMAIN') or socket.gethostname()}\\{getpass.getuser()}" - }) + if not SYSTEM_SERVICE_MODE: + import getpass + await sio.emit('collector_status', { + 'agent_id': AGENT_ID, + 'hostname': socket.gethostname(), + 'active': True, + 'last_user': f"{os.environ.get('USERDOMAIN') or socket.gethostname()}\\{getpass.getuser()}" + }) + else: + await sio.emit('collector_status', { + 'agent_id': AGENT_ID, + 'hostname': socket.gethostname(), + 'active': True, + }) except Exception: pass diff --git a/Data/Server/server.py b/Data/Server/server.py index 6f6132e..c10d7b6 100644 --- a/Data/Server/server.py +++ b/Data/Server/server.py @@ -2243,20 +2243,65 @@ def handle_quick_job_result(data): @socketio.on("collector_status") def handle_collector_status(data): - """Collector agent reports activity and optional last_user.""" + """Collector agent reports activity and optional last_user. + + To avoid flapping of summary.last_user between the SYSTEM service and the + interactive user helper, we only accept last_user updates that look like a + real interactive user and, by preference, only from the interactive agent + (agent_id ending with "-script"). Machine accounts (..$) and built-in + service principals (SYSTEM/LOCAL SERVICE/NETWORK SERVICE) are ignored. + """ agent_id = (data or {}).get('agent_id') hostname = (data or {}).get('hostname') active = bool((data or {}).get('active')) last_user = (data or {}).get('last_user') if not agent_id: return + rec = registered_agents.setdefault(agent_id, {}) rec['agent_id'] = agent_id if hostname: rec['hostname'] = hostname if active: rec['collector_active_ts'] = time.time() - if last_user and (hostname or rec.get('hostname')): + + # Helper: decide if a reported user string is a real interactive user + def _is_valid_interactive_user(s: str) -> bool: + try: + if not s: + return False + t = str(s).strip() + if not t: + return False + # Reject machine accounts and well-known service identities + upper = t.upper() + if t.endswith('$'): + return False + if any(x in upper for x in ('NT AUTHORITY\\', 'NT SERVICE\\')): + return False + if upper.endswith('\\SYSTEM') or upper.endswith('\\LOCAL SERVICE') or upper.endswith('\\NETWORK SERVICE') or upper == 'ANONYMOUS LOGON': + return False + # Looks acceptable (DOMAIN\\user or user) + return True + except Exception: + return False + + # Prefer interactive/script agent as the source of truth for last_user + is_script_agent = False + try: + is_script_agent = bool((isinstance(agent_id, str) and agent_id.lower().endswith('-script')) or rec.get('is_script_agent')) + except Exception: + is_script_agent = False + + # If we have a usable last_user and a hostname, persist it + if last_user and _is_valid_interactive_user(last_user) and (hostname or rec.get('hostname')): + # If this event is coming from the SYSTEM service agent, ignore it to + # prevent clobbering the interactive user's value. + try: + if isinstance(agent_id, str) and ('-svc-' in agent_id.lower() or agent_id.lower().endswith('-svc')) and not is_script_agent: + return + except Exception: + pass try: host = hostname or rec.get('hostname') conn = _db_conn()