Fixed Last User Race Condition

This commit is contained in:
2025-09-27 21:53:23 -06:00
parent d8bb277a80
commit 91aafc305d
2 changed files with 82 additions and 18 deletions

View File

@@ -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

View File

@@ -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()