#!/usr/bin/env python3 # rebuild-and-push.py — rebuild the "machines not in GuruRMM" list using the CORRECT # ScreenConnect online signal (ActiveConnections ProcessType==2 = guest agent connected; # GuestInfoUpdateTime for recency), NOT Syncro's stale last_synced_at. Then push the # generic STAGING installer to every machine that is ONLINE right now (it will actually # land instead of queuing). Machines enroll into Staging -> reassign-staging.py sorts them. # # Env: RMM, TOK (GuruRMM), SK (Syncro), SC_SECRET (ScreenConnect). --dry = no pushes. import json,urllib.request,ssl,os,sys,base64,datetime,urllib.parse sys.stdout.reconfigure(encoding='utf-8',errors='replace') RMM=os.environ["RMM"]; TOK=os.environ["TOK"]; SK=os.environ["SK"]; SEC=os.environ["SC_SECRET"] ctx=ssl.create_default_context(); DRY="--dry" in sys.argv SB="https://computerguru.screenconnect.com/App_Extensions/2d558935-686a-4bd0-9991-07539f5fe749/Service.ashx" STAGING_ONELINER="irm 'https://rmm.azcomputerguru.com/install/DARK-STORM-3150/windows'|iex" ENC=base64.b64encode(STAGING_ONELINER.encode("utf-16-le")).decode() PUSHCMD=f"powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand {ENC}" def scp(m,b): req=urllib.request.Request(f"{SB}/{m}",data=json.dumps(b).encode(),method="POST",headers={"CTRLAuthHeader":SEC,"Origin":"https://computerguru.screenconnect.com","Content-Type":"application/json"}) try:return json.loads(urllib.request.urlopen(req,context=ctx,timeout=25).read().decode("utf-8","replace")) except Exception as e:return {"__err":str(e)} def rget(p): return json.loads("".join(c for c in urllib.request.urlopen(urllib.request.Request(f"{RMM}{p}",headers={"Authorization":f"Bearer {TOK}"}),context=ctx,timeout=30).read().decode("utf-8","replace") if ord(c)>=32 or c in "\t\n\r")) def assets(cid): raw=urllib.request.urlopen(urllib.request.Request(f"https://computerguru.syncromsp.com/api/v1/customer_assets?customer_id={cid}&per_page=100&api_key={SK}"),context=ctx,timeout=25).read() raw="".join(chr(b) for b in raw if b>=32 or b in (9,10,13)) return [a["name"] for a in json.loads(raw).get("assets",[]) if a.get("asset_type")=="Syncro Device" and a.get("name")] def online(s): return any(c.get("ProcessType")==2 for c in (s.get("ActiveConnections") or [])) def recency_days(s): for f in ("GuestInfoUpdateTime","LastGuestConnectedEventTime"): t=s.get(f) if t and not str(t).startswith("0001"): try: return (datetime.datetime.utcnow()-datetime.datetime.strptime(str(t)[:19],"%Y-%m-%dT%H:%M:%S")).days except: pass return None rmm=set(a["hostname"].lower() for a in rget("/api/agents") if a.get("hostname")) tgts=json.load(open("projects/gps-rmm-audit/targets.json"))["clients"] groups={"ONLINE now":[],"active <=14d":[],"stale 15-45d":[],"dead/no-session":[]} pushed=0; failed=0 for t in tgts: for h in assets(t["cid"]): if h.lower() in rmm: continue ss=scp("GetSessionsByName",{"sessionName":h}) if not (isinstance(ss,list) and ss): groups["dead/no-session"].append((t["client"],h,"no SC")); continue if len(ss)>1: continue # ambiguous shared name -> skip s=ss[0]; d=recency_days(s) if online(s): groups["ONLINE now"].append((t["client"],h,f"{d}d" if d is not None else "?")) if not DRY: r=scp("SendCommandToSession",[s["SessionID"],PUSHCMD]) if isinstance(r,dict) and r.get("__err"): failed+=1 else: pushed+=1 elif d is not None and d<=14: groups["active <=14d"].append((t["client"],h,f"{d}d")) elif d is not None and d<=45: groups["stale 15-45d"].append((t["client"],h,f"{d}d")) else: groups["dead/no-session"].append((t["client"],h,f"{d}d" if d is not None else "old")) out=["# Machines not in GuruRMM — by ScreenConnect reachability (CORRECTED)","", "Online = ActiveConnections ProcessType==2 (guest agent connected). Recency = GuestInfoUpdateTime.", f"Rebuilt {datetime.date(2026,7,4)}. Staging installer pushed to all ONLINE machines (land in Staging -> reassign-staging.py).",""] for g,items in groups.items(): out.append(f"## {g} ({len(items)})") for c,h,d in sorted(items): out.append(f"- {h} [{c}] ({d})") out.append("") open("projects/gps-rmm-audit/needs-screenconnect.md","w",encoding="utf-8").write("\n".join(out)) print(f"ONLINE now: {len(groups['ONLINE now'])} | active<=14d: {len(groups['active <=14d'])} | stale15-45: {len(groups['stale 15-45d'])} | dead: {len(groups['dead/no-session'])}") print(f"Staging installer pushed: {pushed} (failed {failed}){' [DRY]' if DRY else ''}")